[AAE-46514] - Migrating to Pnpm to increase safety

This commit is contained in:
VitoAlbano
2026-06-04 09:26:32 +01:00
parent eab5abb507
commit f8f518a87e
22 changed files with 21200 additions and 39228 deletions
+12 -6
View File
@@ -33,11 +33,13 @@ runs:
- name: Set consistent machine ID for Nx cache
shell: bash
run: echo "nx-github-actions" | sudo tee /etc/machine-id > /dev/null
- name: install NPM
- name: Setup pnpm
uses: pnpm/action-setup@v4
- name: Setup Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version-file: '.nvmrc'
cache-dependency-path: package-lock.json
cache: 'pnpm'
- name: get latest tag sha
if: ${{ inputs.full-setup == 'true' }}
id: tag-sha
@@ -46,15 +48,19 @@ runs:
if: ${{ inputs.full-setup == 'true' }}
id: set-npm-tag
uses: ./.github/actions/set-npm-tag
- name: Cache node modules
- name: Get pnpm store directory
id: pnpm-cache
shell: bash
run: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_OUTPUT
- name: Cache pnpm store
id: node-modules-cache
if: ${{ inputs.enable-node-modules-cache == 'true' }}
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: node_modules
key: ${{ runner.os }}-node-modules-${{ hashFiles('.nvmrc') }}-${{ hashFiles('package-lock.json') }}
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-node-modules-${{ hashFiles('.nvmrc') }}-
${{ runner.os }}-pnpm-store-
- name: Restore nx cache
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
+1 -2
View File
@@ -11,7 +11,6 @@ on:
env:
NODE_OPTIONS: "--max-old-space-size=5120"
ADF_SKIP_SECURITY_CHECK: 1
jobs:
build:
@@ -31,7 +30,7 @@ jobs:
full-setup: 'false'
- name: Install dependencies
if: ${{ steps.setup-env.outputs.node-modules-cache-hit != 'true' }}
run: npm ci
run: pnpm install --frozen-lockfile
- name: Build affected libs
env:
BASE_REF: ${{ inputs.base_ref }}
+5 -6
View File
@@ -33,7 +33,6 @@ concurrency:
env:
GH_COMMIT: ${{ github.sha }}
NODE_OPTIONS: "--max-old-space-size=5120"
ADF_SKIP_SECURITY_CHECK: 1
jobs:
pre-checks:
@@ -177,11 +176,11 @@ jobs:
cache-suffix: setup
- name: Install dependencies
if: ${{ steps.setup-env.outputs.node-modules-cache-hit != 'true' }}
run: npm ci
run: pnpm install --frozen-lockfile
- name: Bundle
run: |
npm run bundle:js-api
npm run bundle:cli
pnpm run bundle:js-api
pnpm run bundle:cli
- name: Save nx cache
if: ${{ success() }}
uses: ./.github/actions/save-nx-cache
@@ -206,7 +205,7 @@ jobs:
full-setup: 'false'
- name: Install dependencies
if: ${{ steps.setup-env.outputs.node-modules-cache-hit != 'true' }}
run: npm ci
run: pnpm install --frozen-lockfile
- name: Run lint
env:
BASE_REF: ${{ github.base_ref || 'develop' }}
@@ -242,7 +241,7 @@ jobs:
full-setup: 'false'
- name: Install dependencies
if: ${{ steps.setup-env.outputs.node-modules-cache-hit != 'true' }}
run: npm ci
run: pnpm install --frozen-lockfile
- name: Build Storybook
env:
BASE_REF: ${{ github.base_ref || 'develop' }}
+7 -8
View File
@@ -40,7 +40,6 @@ env:
GH_BUILD_NUMBER: ${{ github.run_id }}
LOG_LEVEL: "ERROR"
NODE_OPTIONS: "--max-old-space-size=5120"
ADF_SKIP_SECURITY_CHECK: 1
jobs:
setup:
@@ -58,9 +57,9 @@ jobs:
enable-node-modules-cache: false
- name: install
run: |
npm ci
npm run bundle:js-api
npm run bundle:cli
pnpm install --frozen-lockfile
pnpm run bundle:js-api
pnpm run bundle:cli
- uses: ./.github/actions/upload-node-modules-and-artifacts
release-npm:
@@ -101,15 +100,15 @@ jobs:
setMigrations();
- name: build libraries
run: |
npm run build:libs
npm run build:schematics
pnpm run build:libs
pnpm run build:schematics
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
name: release libraries GH registry
with:
node-version-file: '.nvmrc'
registry-url: 'https://npm.pkg.github.com'
scope: '@alfresco'
- run: npm run publish -- --tag=${{ steps.setup.outputs.npm-tag }}
- run: pnpm run publish -- --tag=${{ steps.setup.outputs.npm-tag }}
env:
NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
@@ -118,7 +117,7 @@ jobs:
node-version-file: '.nvmrc'
registry-url: 'https://${{ vars.NPM_REGISTRY_ADDRESS }}'
scope: '@alfresco'
- run: npm run publish -- --tag=${{ steps.setup.outputs.npm-tag }}
- run: pnpm run publish -- --tag=${{ steps.setup.outputs.npm-tag }}
create-git-tag:
runs-on: ubuntu-latest
+2 -3
View File
@@ -10,7 +10,6 @@ on:
default: 'develop'
env:
ADF_SKIP_SECURITY_CHECK: 1
jobs:
generate-affected-matrix:
@@ -31,7 +30,7 @@ jobs:
cache-suffix: test-matrix
- name: Install dependencies
if: ${{ steps.setup-env.outputs.node-modules-cache-hit != 'true' }}
run: npm ci
run: pnpm install --frozen-lockfile
- name: Generate affected projects matrix
id: set-matrix
env:
@@ -77,7 +76,7 @@ jobs:
full-setup: 'false'
- name: Install dependencies
if: ${{ steps.setup-env.outputs.node-modules-cache-hit != 'true' }}
run: npm ci
run: pnpm install --frozen-lockfile
- name: Run unit tests for ${{ matrix.project }}
env:
NODE_OPTIONS: "--max-old-space-size=5120"
+6
View File
@@ -1,4 +1,10 @@
#!/bin/sh
export NODE_OPTIONS=--max_old_space_size=8192
# Check new packages against security databases
if git diff --cached --name-only | grep -q "pnpm-lock.yaml"; then
node scripts/check-new-packages.mjs
fi
lint-staged
+14 -17
View File
@@ -23,29 +23,26 @@ for full details on what you may need to install before using ADF.
## Installation
This project uses **pnpm** for package management with built-in supply chain attack protection.
```bash
npm install # or npm ci
pnpm install # install all packages
pnpm run add <package> # add a new package (with security check)
```
This project has built-in supply chain attack protection. When you run `npm install`:
### Supply Chain Security
1. **Preinstall check** scans both `package.json` AND `package-lock.json` against OSV + GitHub Advisory databases
2. If malicious packages detected → installation blocked BEFORE any code runs
3. Packages install with `--ignore-scripts` (lifecycle scripts disabled for security)
4. **Post-install check** re-runs the security scan (defense in depth) and deletes `node_modules` if violations found
5. Trusted packages (esbuild, nx, husky, etc.) are rebuilt via `npm rebuild` to run their lifecycle scripts
**Layer 1: pnpm script blocking**
- All lifecycle scripts (postinstall, etc.) are blocked by default
- Only trusted packages in `pnpm-workspace.yaml` can run scripts
- Protects during `pnpm install` and `pnpm add`
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.
**Layer 2: Security database check**
- `pnpm run add` checks packages against OSV and GitHub Advisory databases BEFORE installing
- Pre-commit hook blocks commits containing known malicious packages
To disable security checks (e.g., in CI environments):
```bash
ADF_SKIP_SECURITY_CHECK=1 npm install
```
To keep `node_modules` on violations (for debugging):
```bash
ADF_SECURITY_KEEP_NODE_MODULES=1 npm install
```
**Layer 3: npm blocked**
- Running `npm install` will fail - enforces pnpm usage
## Components
-37509
View File
File diff suppressed because it is too large Load Diff
+4 -2
View File
@@ -4,8 +4,9 @@
"version": "8.5.0",
"author": "Hyland Software, Inc. and its affiliates",
"scripts": {
"preinstall": "node scripts/preinstall-check.mjs",
"prepare": "node scripts/postinstall-security.mjs",
"preinstall": "npx only-allow pnpm",
"prepare": "husky && node scripts/post-install-check.mjs",
"add": "node scripts/safe-add.mjs",
"bundle:js-api": "nx run js-api:bundle",
"bundle:cli": "nx run cli:bundle",
"test:affected": "nx affected:test",
@@ -159,6 +160,7 @@
"engines": {
"node": ">=24.14.0"
},
"packageManager": "pnpm@11.5.0",
"module": "./index.js",
"typings": "./index.d.ts"
}
+1
View File
@@ -0,0 +1 @@
Check pnpm docs for onlyBuiltDependencies
+20618
View File
File diff suppressed because it is too large Load Diff
+31
View File
@@ -0,0 +1,31 @@
allowBuilds:
'@parcel/watcher': true
esbuild: true
lmdb: true
msgpackr-extract: true
nx: true
unrs-resolver: true
sharp: true
node-sass: true
sass: true
pdfjs-dist: true
canvas: true
bcrypt: true
sqlite3: true
better-sqlite3: true
puppeteer: true
playwright: true
fsevents: true
husky: true
core-js: true
core-js-pure: true
'@esbuild/darwin-arm64': true
'@esbuild/darwin-x64': true
'@esbuild/linux-x64': true
'@esbuild/win32-x64': true
'@nx/nx-darwin-arm64': true
'@nx/nx-darwin-x64': true
'@nx/nx-linux-x64-gnu': true
'@nx/nx-linux-x64-musl': true
'@nx/nx-win32-x64-msvc': true
'@swc/core': true
+188
View File
@@ -0,0 +1,188 @@
#!/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-commit security check for new packages.
*
* Checks any new/changed packages in pnpm-lock.yaml against OSV and GitHub
* Advisory databases to prevent committing known malicious packages.
*/
import { execSync } from 'node:child_process';
const MALWARE_KEYWORDS = ['malware', 'malicious', 'compromised', 'supply chain', 'backdoor'];
function getChangedPackages() {
try {
const diff = execSync('git diff --cached pnpm-lock.yaml', { encoding: 'utf-8' });
const addedPackages = [];
const packageRegex = /^\+\s+'?([^:'\s]+)'?:\s*$/gm;
const versionRegex = /^\+\s+version:\s+'?([^'\s]+)'?/gm;
let match;
const lines = diff.split('\n');
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (line.startsWith('+') && !line.startsWith('+++')) {
const pkgMatch = line.match(/^\+\s+'?\/([^']+)'?:/);
if (pkgMatch) {
const fullPath = pkgMatch[1];
const name = fullPath.includes('@') && !fullPath.startsWith('@')
? fullPath.split('@')[0]
: fullPath.replace(/@[^/]+$/, '');
const version = fullPath.match(/@(\d+\.\d+\.\d+[^/]*)/)?.[1];
if (name && version) {
addedPackages.push({ name, version });
}
}
}
}
return addedPackages;
} catch {
return [];
}
}
async function checkWithOSV(packages) {
if (!packages.length) return [];
const findings = [];
try {
const response = await fetch('https://api.osv.dev/v1/querybatch', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
queries: packages.map(pkg => ({
package: { name: pkg.name, ecosystem: 'npm' },
version: pkg.version
}))
}),
signal: AbortSignal.timeout(30000)
});
if (!response.ok) return findings;
const data = await response.json();
for (let i = 0; i < (data.results || []).length; i++) {
const result = data.results[i];
for (const vuln of result.vulns || []) {
const summary = [vuln.summary || '', vuln.details || ''].join(' ').toLowerCase();
if (MALWARE_KEYWORDS.some(kw => summary.includes(kw))) {
findings.push({
package: packages[i].name,
version: packages[i].version,
reason: vuln.summary || 'Malware detected',
source: 'OSV'
});
}
}
}
} catch {
// Network errors are non-fatal
}
return findings;
}
async function checkWithGitHub(packages) {
if (!packages.length) return [];
const findings = [];
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 findings;
const advisories = await response.json();
for (const pkg of packages) {
for (const advisory of advisories) {
for (const vuln of advisory.vulnerabilities || []) {
if (vuln.package?.name === pkg.name) {
findings.push({
package: pkg.name,
version: pkg.version,
reason: advisory.summary || 'Malware advisory',
source: 'GitHub Advisory'
});
}
}
}
}
} catch {
// Network errors are non-fatal
}
return findings;
}
async function main() {
const changedPackages = getChangedPackages();
if (!changedPackages.length) {
process.exit(0);
}
console.log(`\n🔍 Checking ${changedPackages.length} new/changed packages against security databases...\n`);
const [osvFindings, ghFindings] = await Promise.all([
checkWithOSV(changedPackages),
checkWithGitHub(changedPackages)
]);
const allFindings = [...osvFindings, ...ghFindings];
if (allFindings.length > 0) {
console.error('='.repeat(70));
console.error('🚨 MALICIOUS PACKAGES DETECTED - COMMIT BLOCKED');
console.error('='.repeat(70) + '\n');
for (const finding of allFindings) {
console.error(`${finding.package}@${finding.version}`);
console.error(` ${finding.reason} (${finding.source})\n`);
}
console.error('Remove these packages before committing:');
console.error(' pnpm remove <package-name>\n');
console.error('='.repeat(70) + '\n');
process.exit(1);
}
console.log('✅ All new packages passed security checks\n');
}
try {
await main();
} catch (error) {
console.error('Security check error:', error.message);
process.exit(0);
}
-498
View File
@@ -1,498 +0,0 @@
#!/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.
*/
/**
* Security check script - blocks npm install if compromised packages are detected.
*
* This script:
* 1. Fetches known malicious packages from multiple sources:
* - OSV (Open Source Vulnerabilities) - Google's aggregated database
* - GitHub Advisory Database - GitHub's security advisories
* - Meterian CLI (optional) - additional vulnerability coverage
* 2. Checks both package.json and package-lock.json
* 3. Blocks installation if a compromised package is found
*
* Cache: Results are cached locally for 24 hours to avoid slowing down installs.
*
* Set ADF_SKIP_SECURITY_CHECK=1 to disable (useful for CI environments).
*/
if (process.env.ADF_SKIP_SECURITY_CHECK === '1' || process.env.ADF_SKIP_SECURITY_CHECK === 'true') {
console.log('🔒 ADF Security Check: Skipped (ADF_SKIP_SECURITY_CHECK=1)\n');
process.exit(0);
}
import { readFileSync, writeFileSync, mkdirSync, rmSync, statSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { fetchMalwareData as fetchFromOSV } from './security-providers/osv-provider.mjs';
import { fetchMalwareData as fetchFromGitHub } from './security-providers/github-provider.mjs';
import { checkVulnerabilities as checkWithMeterian } from './security-providers/meterian-provider.mjs';
import { FALLBACK_BLOCKED_PACKAGES } from './security-providers/fallback-list.mjs';
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_FILE = join(CACHE_DIR, 'blocked-packages.json');
const CACHE_TTL_MS = 24 * 60 * 60 * 1000;
// ============================================================================
// FILE UTILITIES
// ============================================================================
function readJsonFile(filePath) {
try {
return JSON.parse(readFileSync(filePath, 'utf8'));
} catch {
return null;
}
}
function writeJsonFile(filePath, data) {
try {
writeFileSync(filePath, JSON.stringify(data, null, 2));
return true;
} catch {
return false;
}
}
function directoryExists(path) {
try {
return statSync(path).isDirectory();
} catch {
return false;
}
}
function ensureDirectory(path) {
try {
mkdirSync(path, { recursive: true });
return true;
} catch {
return false;
}
}
function removeDirectory(path) {
try {
rmSync(path, { recursive: true, force: true });
return true;
} catch {
return false;
}
}
// ============================================================================
// DEPENDENCY EXTRACTION
// ============================================================================
function extractDependenciesFromLockfile(lockfilePath) {
const lockfile = readJsonFile(lockfilePath);
if (!lockfile?.packages) {
return [];
}
return Object.entries(lockfile.packages)
.filter(([path, info]) => path && path !== '' && info.version)
.map(([path, info]) => ({
name: path.replace(/^node_modules\//, '').replace(/^.*node_modules\//, ''),
version: info.version
}));
}
function extractDependenciesFromPackageJson(packageJsonPath, existingDependencies) {
const packageJson = readJsonFile(packageJsonPath);
if (!packageJson) {
return [];
}
const allDeps = {
...packageJson.dependencies,
...packageJson.devDependencies
};
return Object.entries(allDeps)
.filter(([, versionSpec]) => typeof versionSpec === 'string' && !versionSpec.startsWith('file:'))
.map(([name, versionSpec]) => ({
name,
version: versionSpec.replace(/^[\^~>=<]*/, '').split(' ')[0]
}))
.filter(dep => dep.version && !existingDependencies.some(
existing => existing.name === dep.name && existing.version === dep.version
));
}
function getProjectDependencies() {
const lockfileDeps = extractDependenciesFromLockfile(join(ROOT_DIR, 'package-lock.json'));
const packageJsonDeps = extractDependenciesFromPackageJson(join(ROOT_DIR, 'package.json'), lockfileDeps);
return [...lockfileDeps, ...packageJsonDeps];
}
// ============================================================================
// CACHE MANAGEMENT
// ============================================================================
function readCache() {
const cache = readJsonFile(CACHE_FILE);
if (!cache) {
return null;
}
const isExpired = Date.now() - cache.timestamp >= CACHE_TTL_MS;
return isExpired ? null : cache;
}
function writeCache(packages, sources) {
ensureDirectory(CACHE_DIR);
writeJsonFile(CACHE_FILE, {
timestamp: Date.now(),
sources,
packages
});
}
// ============================================================================
// MALWARE DATABASE AGGREGATION
// ============================================================================
function mergeResults(target, source) {
for (const [name, data] of Object.entries(source)) {
if (target[name]) {
target[name].versions = [...new Set([...target[name].versions, ...(data.versions || [])])];
target[name].versionRanges = [...new Set([
...(target[name].versionRanges || []),
...(data.versionRanges || [])
])];
} else {
target[name] = data;
}
}
}
async function fetchMalwareDatabase(projectDependencies) {
const cache = readCache();
if (cache) {
console.log(' Using cached security database...');
console.log(` (${cache.sources?.join(', ') || 'fallback'} - cached ${Math.round((Date.now() - cache.timestamp) / 60000)} min ago)`);
return { packages: cache.packages, sources: cache.sources || ['fallback'] };
}
console.log(' Fetching latest security databases...\n');
const packages = { ...FALLBACK_BLOCKED_PACKAGES };
const sources = ['fallback'];
const osvResults = await fetchFromOSV(projectDependencies);
if (Object.keys(osvResults).length > 0) {
sources.push('OSV');
mergeResults(packages, osvResults);
}
const githubResults = await fetchFromGitHub();
if (Object.keys(githubResults).length > 0) {
sources.push('GitHub');
mergeResults(packages, githubResults);
}
writeCache(packages, sources);
return { packages, sources };
}
// ============================================================================
// VERSION CHECKING
// ============================================================================
function compareVersions(versionA, versionB) {
const partsA = versionA.replace(/^v/, '').split('.').map(part => parseInt(part, 10) || 0);
const partsB = versionB.replace(/^v/, '').split('.').map(part => parseInt(part, 10) || 0);
const maxLength = Math.max(partsA.length, partsB.length);
let result = 0;
for (let index = 0; index < maxLength && result === 0; index++) {
const numA = partsA[index] || 0;
const numB = partsB[index] || 0;
result = Math.sign(numA - numB);
}
return result;
}
function matchesVersionRange(version, range) {
const conditions = range.split(',').map(condition => condition.trim());
for (const condition of conditions) {
const match = condition.match(/^(>=|<=|>|<|=)?\s*(\S+)$/);
if (!match) continue;
const [, operator = '=', targetVersion] = match;
const comparison = compareVersions(version, targetVersion.trim());
const operatorChecks = {
'=': comparison === 0,
'>': comparison > 0,
'>=': comparison >= 0,
'<': comparison < 0,
'<=': comparison <= 0
};
if (!operatorChecks[operator]) {
return false;
}
}
return true;
}
function isVersionBlocked(version, blockedEntry) {
if (blockedEntry.versions?.some(blocked => blocked === version)) {
return true;
}
if (blockedEntry.versionRanges?.some(range => matchesVersionRange(version, range))) {
return true;
}
return false;
}
// ============================================================================
// VIOLATION DETECTION
// ============================================================================
function checkDependenciesForViolations(dependencies, blockedPackages, source) {
if (!dependencies) return [];
return Object.entries(dependencies)
.filter(([name]) => blockedPackages[name])
.map(([name, versionSpec]) => ({
name,
version: versionSpec.replace(/^[\^~>=<]*/, '').split(' ')[0],
blockedEntry: blockedPackages[name]
}))
.filter(({ version, blockedEntry }) => isVersionBlocked(version, blockedEntry))
.map(({ name, version, blockedEntry }) => ({
package: name,
version,
reason: blockedEntry.reason,
source
}));
}
function checkLockfileForViolations(packages, blockedPackages) {
if (!packages) return [];
return Object.entries(packages)
.filter(([path]) => path && path !== '')
.map(([path, info]) => ({
name: path.replace(/^node_modules\//, '').replace(/^.*node_modules\//, ''),
version: info.version
}))
.filter(({ name, version }) => blockedPackages[name] && version && isVersionBlocked(version, blockedPackages[name]))
.map(({ name, version }) => ({
package: name,
version,
reason: blockedPackages[name].reason,
source: 'package-lock.json (transitive)'
}));
}
function deduplicateViolations(violations) {
return violations.filter((violation, index, array) =>
array.findIndex(other =>
other.package === violation.package && other.version === violation.version
) === index
);
}
// ============================================================================
// METERIAN INTEGRATION
// ============================================================================
function prepareDependenciesForMeterian(lockfilePackages) {
return Object.entries(lockfilePackages)
.filter(([path, info]) => path && info.version)
.map(([path, info]) => ({
name: path.replace(/^node_modules\//, '').replace(/^.*node_modules\//, ''),
version: info.version
}))
.slice(0, 500);
}
function formatMeterianFindings(meterianResult) {
return (meterianResult.vulnerable || []).map(vulnerability => ({
package: vulnerability.name,
version: vulnerability.version,
reason: `${vulnerability.severity}: ${vulnerability.id}${vulnerability.safeVersions?.length ? ` (safe: ${vulnerability.safeVersions[0]})` : ''}`,
source: 'Meterian'
}));
}
function handleMeterianFindings(findings, shouldBlock) {
if (shouldBlock) {
return findings;
}
if (findings.length > 0) {
console.log('\n⚠️ Meterian found vulnerabilities (informational, not blocking):');
for (const finding of findings) {
console.log(` ${finding.package}@${finding.version} - ${finding.reason}`);
}
console.log(' Set ADF_METERIAN_BLOCK=1 to block on these findings.\n');
}
return [];
}
// ============================================================================
// REPORTING
// ============================================================================
function reportViolations(violations) {
console.error('\n' + '='.repeat(70));
console.error('🚨 SECURITY ALERT: MALICIOUS PACKAGES DETECTED 🚨');
console.error('='.repeat(70) + '\n');
console.error('The following packages are known to be COMPROMISED:\n');
for (const violation of violations) {
console.error(` 📦 ${violation.package}@${violation.version}`);
console.error(` ⚠️ ${violation.reason}`);
console.error(` 📍 Found in: ${violation.source}\n`);
}
console.error('='.repeat(70));
console.error('⛔ INSTALLATION BLOCKED FOR YOUR SECURITY');
console.error('='.repeat(70) + '\n');
console.error('These packages contain malicious code that may:');
console.error(' • Steal credentials and environment variables');
console.error(' • Install cryptominers or backdoors');
console.error(' • Exfiltrate sensitive data\n');
}
function handleNodeModulesDeletion() {
const skipDeletion = process.env.ADF_SECURITY_KEEP_NODE_MODULES === '1' ||
process.env.ADF_SECURITY_KEEP_NODE_MODULES === 'true';
const nodeModulesPath = join(ROOT_DIR, 'node_modules');
if (!directoryExists(nodeModulesPath)) {
return;
}
if (skipDeletion) {
console.error('⚠️ Skipping node_modules deletion (ADF_SECURITY_KEEP_NODE_MODULES=1)\n');
return;
}
console.error('🗑️ Removing node_modules to prevent use of compromised packages...');
console.error(' (Set ADF_SECURITY_KEEP_NODE_MODULES=1 to skip deletion)\n');
if (removeDirectory(nodeModulesPath)) {
console.error('✅ node_modules deleted successfully.\n');
} else {
console.error('⚠️ Could not delete node_modules. Please delete it manually.\n');
}
}
function reportRequiredActions() {
console.error('📋 REQUIRED ACTIONS:');
console.error(' 1. Review your package.json for the affected packages');
console.error(' 2. Update to safe versions or remove the packages');
console.error(' 3. Delete package-lock.json and regenerate it');
console.error(' 4. Run npm install again\n');
console.error('📚 More information:');
console.error(' • https://osv.dev');
console.error(' • https://socket.dev/npm/advisories');
console.error(' • https://github.com/advisories\n');
}
function reportSuccess(sources, packageCount) {
const sourceList = sources.join(' + ');
console.log(`✅ Security check passed (checked against ${sourceList}, ${packageCount} blocked packages)`);
console.log('='.repeat(70) + '\n');
}
// ============================================================================
// MAIN
// ============================================================================
async function main() {
console.log('\n' + '='.repeat(70));
console.log('🔒 ADF SECURITY CHECK');
console.log('='.repeat(70));
console.log('Scanning for known supply chain attacks and compromised packages...\n');
const projectDependencies = getProjectDependencies();
const { packages: blockedPackages, sources } = await fetchMalwareDatabase(projectDependencies);
const violations = [];
const packageJson = readJsonFile(join(ROOT_DIR, 'package.json'));
if (packageJson) {
violations.push(...checkDependenciesForViolations(packageJson.dependencies, blockedPackages, 'package.json (dependencies)'));
violations.push(...checkDependenciesForViolations(packageJson.devDependencies, blockedPackages, 'package.json (devDependencies)'));
}
const lockfile = readJsonFile(join(ROOT_DIR, 'package-lock.json'));
if (lockfile?.packages) {
violations.push(...checkLockfileForViolations(lockfile.packages, blockedPackages));
}
if (lockfile?.packages) {
const meterianDeps = prepareDependenciesForMeterian(lockfile.packages);
const meterianResult = await checkWithMeterian(meterianDeps, ROOT_DIR);
const meterianFindings = formatMeterianFindings(meterianResult);
const shouldBlockOnMeterian = process.env.ADF_METERIAN_BLOCK === '1' ||
process.env.ADF_METERIAN_BLOCK === 'true';
violations.push(...handleMeterianFindings(meterianFindings, shouldBlockOnMeterian));
}
const uniqueViolations = deduplicateViolations(violations);
if (uniqueViolations.length > 0) {
reportViolations(uniqueViolations);
handleNodeModulesDeletion();
reportRequiredActions();
process.exit(1);
}
reportSuccess(sources, Object.keys(blockedPackages).length);
process.exit(0);
}
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);
}
+165
View File
@@ -0,0 +1,165 @@
#!/usr/bin/env node
/*!
* @license
* Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved.
*/
/**
* Post-install security check - warns if any installed packages are known malware.
* Runs after pnpm install via the prepare hook.
*/
import { readFileSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = dirname(fileURLToPath(import.meta.url));
const ROOT_DIR = join(__dirname, '..');
const MALWARE_KEYWORDS = ['malware', 'malicious', 'compromised', 'supply chain', 'backdoor'];
function getInstalledPackages() {
try {
const lockfile = readFileSync(join(ROOT_DIR, 'pnpm-lock.yaml'), 'utf-8');
const packages = [];
const regex = /^\s+'?\/([^'(]+)'/gm;
let match;
while ((match = regex.exec(lockfile)) !== null) {
const fullPath = match[1];
const lastAt = fullPath.lastIndexOf('@');
if (lastAt > 0) {
const name = fullPath.substring(0, lastAt);
const version = fullPath.substring(lastAt + 1);
packages.push({ name, version });
}
}
return packages;
} catch {
return [];
}
}
async function checkWithOSV(packages) {
const findings = [];
const batches = [];
for (let i = 0; i < packages.length; i += 1000) {
batches.push(packages.slice(i, i + 1000));
}
for (const batch of batches) {
try {
const response = await fetch('https://api.osv.dev/v1/querybatch', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
queries: batch.map(pkg => ({
package: { name: pkg.name, ecosystem: 'npm' },
version: pkg.version
}))
}),
signal: AbortSignal.timeout(30000)
});
if (!response.ok) continue;
const data = await response.json();
for (let i = 0; i < (data.results || []).length; i++) {
for (const vuln of data.results[i].vulns || []) {
const summary = [vuln.summary || '', vuln.details || ''].join(' ').toLowerCase();
if (MALWARE_KEYWORDS.some(kw => summary.includes(kw))) {
findings.push({
name: batch[i].name,
version: batch[i].version,
reason: vuln.summary || 'Malware detected',
source: 'OSV'
});
}
}
}
} catch {}
}
return findings;
}
async function checkWithGitHub(packages) {
const findings = [];
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 findings;
const advisories = await response.json();
const malwareNames = new Set();
for (const advisory of advisories) {
for (const vuln of advisory.vulnerabilities || []) {
if (vuln.package?.name) {
malwareNames.add(vuln.package.name);
}
}
}
for (const pkg of packages) {
if (malwareNames.has(pkg.name)) {
findings.push({
name: pkg.name,
version: pkg.version,
reason: 'Known malware package',
source: 'GitHub Advisory'
});
}
}
} catch {}
return findings;
}
async function main() {
const packages = getInstalledPackages();
if (!packages.length) {
return;
}
const [osvFindings, ghFindings] = await Promise.all([
checkWithOSV(packages),
checkWithGitHub(packages)
]);
const allFindings = [...osvFindings, ...ghFindings];
// Dedupe
const unique = allFindings.filter((f, i, arr) =>
arr.findIndex(x => x.name === f.name && x.version === f.version) === i
);
if (unique.length > 0) {
console.error('\n' + '!'.repeat(70));
console.error('🚨 WARNING: MALICIOUS PACKAGES DETECTED');
console.error('!'.repeat(70) + '\n');
for (const finding of unique) {
console.error(`${finding.name}@${finding.version}`);
console.error(` ${finding.reason} (${finding.source})\n`);
}
console.error('Remove these packages immediately:');
console.error(' pnpm remove <package-name>\n');
console.error('!'.repeat(70) + '\n');
process.exit(1);
}
}
main().catch(() => {});
-176
View File
@@ -1,176 +0,0 @@
#!/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.
*/
/**
* Post-install Security Script
*
* 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)
* 2. Rebuilds trusted packages that need native bindings
* 3. Sets up husky
*/
import { execSync } from 'node:child_process';
import { statSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = dirname(fileURLToPath(import.meta.url));
const ROOT_DIR = join(__dirname, '..');
const NPM_PATH = process.env.npm_execpath || 'npm';
const NPX_CMD = NPM_PATH.endsWith('npm-cli.js')
? `"${process.execPath}" "${NPM_PATH.replace('npm-cli.js', 'npx-cli.js')}"`
: 'npx';
const TRUSTED_PACKAGES = [
'esbuild',
'sharp',
'node-sass',
'sass',
'pdfjs-dist',
'canvas',
'bcrypt',
'sqlite3',
'better-sqlite3',
'puppeteer',
'playwright',
'fsevents',
'nx',
'husky',
'core-js',
'core-js-pure'
];
const TRUSTED_SCOPED_PACKAGES = [
'@esbuild/darwin-arm64',
'@esbuild/darwin-x64',
'@esbuild/linux-x64',
'@esbuild/win32-x64',
'@parcel/watcher',
'@nx/nx-darwin-arm64',
'@nx/nx-darwin-x64',
'@nx/nx-linux-x64-gnu',
'@nx/nx-linux-x64-musl',
'@nx/nx-win32-x64-msvc',
'@swc/core'
];
function directoryExists(path) {
try {
return statSync(path).isDirectory();
} catch {
return false;
}
}
function run(command, options = {}) {
try {
execSync(command, {
stdio: 'inherit',
cwd: ROOT_DIR,
...options
});
return true;
} catch {
return false;
}
}
function getInstalledTrustedPackages() {
const nodeModulesPath = join(ROOT_DIR, 'node_modules');
if (!directoryExists(nodeModulesPath)) {
return [];
}
const nonScopedInstalled = TRUSTED_PACKAGES
.filter(pkg => directoryExists(join(nodeModulesPath, pkg)));
const scopedInstalled = TRUSTED_SCOPED_PACKAGES
.filter(pkg => {
const [scope, name] = pkg.split('/');
return directoryExists(join(nodeModulesPath, scope, name));
});
return [...new Set([...nonScopedInstalled, ...scopedInstalled])];
}
function runSecurityCheck() {
console.log('Step 1/3: Running security check...\n');
const securityCheckPath = join(__dirname, 'check-security.mjs');
const securityPassed = run(`"${process.execPath}" "${securityCheckPath}"`);
if (!securityPassed) {
console.error('\n❌ Security check failed - installation aborted\n');
process.exit(1);
}
}
function rebuildTrustedPackages() {
console.log('\nStep 2/3: Rebuilding trusted packages...\n');
const trustedInstalled = getInstalledTrustedPackages();
if (!trustedInstalled.length) {
console.log('No trusted packages require rebuilding.\n');
return;
}
console.log('Trusted packages to rebuild:');
for (const pkg of trustedInstalled) {
console.log(`${pkg}`);
}
const npmCmd = NPM_PATH === 'npm' ? 'npm' : `"${process.execPath}" "${NPM_PATH}"`;
run(`${npmCmd} rebuild --ignore-scripts=false ${trustedInstalled.join(' ')}`);
}
function setupHusky() {
console.log('\nStep 3/3: Setting up husky...\n');
const huskyPath = join(ROOT_DIR, 'node_modules', 'husky');
if (directoryExists(huskyPath)) {
run(`${NPX_CMD} husky`);
}
}
async function main() {
console.log('\n' + '='.repeat(70));
console.log('🔒 ADF POST-INSTALL SECURITY');
console.log('='.repeat(70) + '\n');
runSecurityCheck();
rebuildTrustedPackages();
setupHusky();
console.log('='.repeat(70));
console.log('✅ Post-install security complete');
console.log('='.repeat(70) + '\n');
}
try {
await main();
} catch (error) {
console.error('Post-install failed:', error.message);
process.exit(1);
}
-494
View File
@@ -1,494 +0,0 @@
#!/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.
*
* Set ADF_SKIP_SECURITY_CHECK=1 to disable (useful for CI environments).
*/
if (process.env.ADF_SKIP_SECURITY_CHECK === '1' || process.env.ADF_SKIP_SECURITY_CHECK === 'true') {
console.log('🔒 ADF Security Check: Skipped (ADF_SKIP_SECURITY_CHECK=1)\n');
process.exit(0);
}
import { readFileSync, mkdirSync, writeFileSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node: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;
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'
]);
// ============================================================================
// FILE UTILITIES
// ============================================================================
function readJsonFile(filePath) {
try {
return JSON.parse(readFileSync(filePath, 'utf8'));
} catch {
return null;
}
}
function writeJsonFile(filePath, data) {
try {
writeFileSync(filePath, JSON.stringify(data));
return true;
} catch {
return false;
}
}
function ensureDirectory(path) {
try {
mkdirSync(path, { recursive: true });
return true;
} catch {
return false;
}
}
// ============================================================================
// CACHE MANAGEMENT
// ============================================================================
function readCache() {
const data = readJsonFile(join(CACHE_DIR, 'threats.json'));
if (!data || Date.now() - data.timestamp >= CACHE_TTL) {
return null;
}
return {
threats: new Set(data.threats),
ranges: data.ranges || []
};
}
function writeCache(threats, ranges) {
ensureDirectory(CACHE_DIR);
writeJsonFile(join(CACHE_DIR, 'threats.json'), {
timestamp: Date.now(),
threats: [...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) {
batches.push(items.slice(index, index + 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();
}
const malicious = new Set();
try {
const batches = splitIntoBatches(projectDependencies, OSV_BATCH_SIZE);
for (const batch of batches) {
const results = await queryOSVBatch(batch);
processOSVResults(results, malicious);
}
} catch {
// Network errors are non-fatal
}
return malicious;
}
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) {
for (const vuln of advisory.vulnerabilities || []) {
if (vuln.package?.ecosystem === 'npm' && vuln.package?.name && vuln.vulnerable_version_range) {
malicious.add(`${vuln.package.name}:${vuln.vulnerable_version_range}`);
}
}
}
return malicious;
} catch {
return new Set();
}
}
// ============================================================================
// VERSION COMPARISON
// ============================================================================
function compareVersions(versionA, versionB) {
const partsA = versionA.split('.').map(Number);
const partsB = versionB.split('.').map(Number);
const maxLength = Math.max(partsA.length, partsB.length);
let result = 0;
for (let index = 0; index < maxLength && result === 0; index++) {
const numA = partsA[index] || 0;
const numB = partsB[index] || 0;
result = Math.sign(numA - numB);
}
return result;
}
function parseVersionRange(range, version) {
if (!range || !version) return false;
const parts = range.split(',').map(part => part.trim());
for (const part of parts) {
const match = part.match(/^([<>=]+)\s*(\S+)$/);
if (!match) {
if (part === version) return true;
continue;
}
const [, operator, rangeVersion] = match;
const comparison = compareVersions(version, rangeVersion);
const checks = {
'=': comparison === 0,
'<': comparison < 0,
'<=': comparison <= 0,
'>': comparison > 0,
'>=': comparison >= 0
};
if (!checks[operator]) return false;
}
return true;
}
// ============================================================================
// PACKAGE EXTRACTION
// ============================================================================
function extractVersionNumber(versionSpec) {
if (!versionSpec) return null;
const match = versionSpec.match(/(\d+)\.(\d+)\.(\d+)(?:-([a-zA-Z0-9]+(?:\.[a-zA-Z0-9]+)*))?/);
return match ? match[0] : null;
}
function getPackagesFromPackageJson() {
const packageJson = readJsonFile(join(ROOT_DIR, 'package.json'));
if (!packageJson) {
return [];
}
const depTypes = ['dependencies', 'devDependencies', 'optionalDependencies', 'peerDependencies'];
return depTypes
.flatMap(depType => Object.entries(packageJson[depType] || {}))
.filter(([, versionSpec]) =>
typeof versionSpec === 'string' &&
!versionSpec.startsWith('file:') &&
!versionSpec.startsWith('link:')
)
.map(([name, versionSpec]) => ({
name,
version: extractVersionNumber(versionSpec),
source: 'package.json'
}))
.filter(pkg => pkg.version);
}
function getPackagesFromLockfile() {
const lockfile = readJsonFile(join(ROOT_DIR, 'package-lock.json'));
if (!lockfile) {
return [];
}
const packages = [];
if (lockfile.packages) {
for (const [path, info] of Object.entries(lockfile.packages)) {
if (!path || path === '' || !info.version) continue;
packages.push({
name: path.replace(/^node_modules\//, '').replace(/\/node_modules\//g, '/'),
version: info.version,
source: 'lockfile'
});
}
}
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();
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()];
}
// ============================================================================
// VIOLATION DETECTION
// ============================================================================
function findViolations(packages, threats, ghRanges) {
const violations = [];
for (const pkg of packages) {
const exactKey = `${pkg.name}@${pkg.version}`;
if (threats.has(exactKey)) {
violations.push({ ...pkg, detectionSource: 'exact match' });
continue;
}
for (const entry of ghRanges) {
const [name, range] = entry.split(':');
if (pkg.name === name && parseVersionRange(range, pkg.version)) {
violations.push({ ...pkg, detectionSource: 'GitHub Advisory' });
break;
}
}
}
return violations;
}
// ============================================================================
// REPORTING
// ============================================================================
function reportViolations(violations) {
console.log('\n' + '!'.repeat(70));
console.log('🚨 MALICIOUS PACKAGES DETECTED - BLOCKING INSTALLATION');
console.log('!'.repeat(70) + '\n');
for (const pkg of violations) {
console.log(`${pkg.name}@${pkg.version} (${pkg.detectionSource})`);
}
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');
}
// ============================================================================
// MAIN
// ============================================================================
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');
const packages = getAllPackages();
if (!packages.length) {
console.log(' ⚠️ No packages found to check');
console.log('='.repeat(70) + '\n');
process.exit(0);
}
const fromPackageJson = packages.filter(pkg => pkg.source === 'package.json').length;
const fromLockfile = packages.filter(pkg => pkg.source === 'lockfile').length;
console.log(` Checking ${packages.length} packages (${fromPackageJson} from package.json, ${fromLockfile} from lockfile)\n`);
const cache = readCache();
let threats;
let ghRanges;
if (cache) {
threats = cache.threats;
ghRanges = cache.ranges;
console.log(` Using cached security database (${threats.size} exact + ${ghRanges.length} ranges)\n`);
} else {
console.log(' Fetching latest security databases...\n');
const [osvThreats, ghThreats] = await Promise.all([
fetchOSV(packages).then(result => { console.log(` 📡 OSV: ${result.size} malware entries`); return result; }),
fetchGitHubAdvisory().then(result => { console.log(` 📡 GitHub Advisory: ${result.size} malware entries`); return result; })
]);
threats = new Set([...KNOWN_MALICIOUS, ...osvThreats]);
ghRanges = [...ghThreats];
writeCache(threats, ghRanges);
}
const violations = findViolations(packages, threats, ghRanges);
if (violations.length) {
reportViolations(violations);
process.exit(1);
}
console.log(`\n✅ Security check passed (${threats.size} exact + ${ghRanges.length} ranges checked)`);
console.log('='.repeat(70) + '\n');
}
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);
}
+146
View File
@@ -0,0 +1,146 @@
#!/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.
*/
/**
* Safe add - checks packages against security databases before installing.
*
* Usage: pnpm run add <package>[@version] [-- -D]
*/
import { execSync } from 'node:child_process';
const MALWARE_KEYWORDS = ['malware', 'malicious', 'compromised', 'supply chain', 'backdoor'];
function parsePackageArg(arg) {
const match = arg.match(/^(@?[^@]+)(?:@(.+))?$/);
return match ? { name: match[1], version: match[2] || 'latest' } : null;
}
async function resolveVersion(name, version) {
if (version !== 'latest') return version;
try {
const response = await fetch(`https://registry.npmjs.org/${name}/latest`, {
signal: AbortSignal.timeout(5000)
});
if (response.ok) {
const data = await response.json();
return data.version;
}
} catch {}
return null;
}
async function checkOSV(name, version) {
try {
const response = await fetch('https://api.osv.dev/v1/query', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ package: { name, ecosystem: 'npm' }, version }),
signal: AbortSignal.timeout(10000)
});
if (!response.ok) return null;
const data = await response.json();
for (const vuln of data.vulns || []) {
const summary = [vuln.summary || '', vuln.details || ''].join(' ').toLowerCase();
if (MALWARE_KEYWORDS.some(kw => summary.includes(kw))) {
return vuln.summary || 'Malware detected';
}
}
} catch {}
return null;
}
async function checkGitHub(name) {
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 null;
const advisories = await response.json();
for (const advisory of advisories) {
for (const vuln of advisory.vulnerabilities || []) {
if (vuln.package?.name === name) {
return advisory.summary || 'Malware advisory';
}
}
}
} catch {}
return null;
}
async function main() {
const args = process.argv.slice(2);
const packages = args.filter(arg => !arg.startsWith('-'));
const flags = args.filter(arg => arg.startsWith('-')).join(' ');
if (!packages.length) {
console.log('Usage: pnpm run add <package>[@version] [-- -D]');
process.exit(1);
}
console.log('\n🔒 Checking packages against security databases...\n');
for (const arg of packages) {
const pkg = parsePackageArg(arg);
if (!pkg) {
console.error(`Invalid package: ${arg}`);
process.exit(1);
}
const version = await resolveVersion(pkg.name, pkg.version);
if (!version) {
console.error(`Could not resolve version for ${pkg.name}`);
process.exit(1);
}
console.log(` 📦 ${pkg.name}@${version}`);
const [osvResult, ghResult] = await Promise.all([
checkOSV(pkg.name, version),
checkGitHub(pkg.name)
]);
if (osvResult || ghResult) {
console.error(`\n❌ BLOCKED: ${pkg.name}@${version}`);
console.error(` ${osvResult || ghResult}\n`);
process.exit(1);
}
console.log(` ✅ Clean\n`);
}
console.log('Installing...\n');
execSync(`pnpm add ${packages.join(' ')} ${flags}`, { stdio: 'inherit' });
}
try {
await main();
} catch (error) {
console.error('Error:', error.message);
process.exit(1);
}
@@ -1,60 +0,0 @@
/*!
* @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.
*/
/**
* Fallback list of known malicious packages.
* Used when external APIs (OSV, GitHub) are unreachable.
* These are confirmed supply chain attacks (not regular CVEs).
*/
export const FALLBACK_BLOCKED_PACKAGES = {
'@solana/web3.js': {
versions: ['1.95.6', '1.95.7'],
reason: 'Compromised - steals private keys (Dec 2024)'
},
'@lottiefiles/lottie-player': {
versions: ['2.0.5', '2.0.6', '2.0.7'],
reason: 'Compromised - crypto wallet drainer (Oct 2024)'
},
'node-ipc': {
versions: ['9.2.2', '10.1.1', '10.1.2', '10.1.3', '11.0.0', '11.1.0'],
reason: 'Protestware - overwrites files (Mar 2022)'
},
'ua-parser-js': {
versions: ['0.7.29', '0.8.0', '1.0.0'],
reason: 'Compromised - cryptominer and password stealer (Oct 2021)'
},
'coa': {
versions: ['2.0.3', '2.0.4', '2.1.1', '2.1.3', '3.0.1', '3.1.3'],
reason: 'Compromised - password stealer (Nov 2021)'
},
'rc': {
versions: ['1.2.9', '1.3.9', '2.3.9'],
reason: 'Compromised - exfiltrates environment variables (Nov 2021)'
},
'colors': {
versions: ['1.4.1', '1.4.44-liberty-2'],
reason: 'Sabotage - infinite loop (Jan 2022)'
},
'faker': {
versions: ['6.6.6'],
reason: 'Sabotage - no functionality (Jan 2022)'
},
'event-stream': {
versions: ['3.3.6'],
reason: 'Compromised - bitcoin wallet theft (Nov 2018)'
}
};
@@ -1,91 +0,0 @@
/*!
* @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.
*/
const GITHUB_ADVISORY_API = 'https://api.github.com/advisories';
const REQUEST_TIMEOUT_MS = 15000;
function extractExactVersion(versionRange) {
const exactMatch = versionRange.match(/^=\s*(.+)$/);
return exactMatch ? exactMatch[1].trim() : null;
}
function processAdvisoryVulnerability(vulnerability, advisory, registry) {
const packageName = vulnerability.package?.name;
if (!packageName || vulnerability.package?.ecosystem !== 'npm') {
return;
}
if (!registry[packageName]) {
registry[packageName] = { versions: [], versionRanges: [], reason: '', source: 'GitHub' };
}
if (vulnerability.vulnerable_version_range) {
registry[packageName].versionRanges.push(vulnerability.vulnerable_version_range);
const exactVersion = extractExactVersion(vulnerability.vulnerable_version_range);
if (exactVersion) {
registry[packageName].versions.push(exactVersion);
}
}
registry[packageName].reason = advisory.summary || 'Malware detected by GitHub Advisory';
}
function processAdvisory(advisory, registry) {
const vulnerabilities = advisory.vulnerabilities || [];
for (const vulnerability of vulnerabilities) {
processAdvisoryVulnerability(vulnerability, advisory, registry);
}
}
export async function fetchMalwareData() {
console.log(' 📡 Fetching from GitHub Advisory...');
const malwareRegistry = {};
try {
const response = await fetch(
`${GITHUB_ADVISORY_API}?ecosystem=npm&type=malware&per_page=100`,
{
headers: {
'Accept': 'application/vnd.github+json',
'X-GitHub-Api-Version': '2022-11-28'
},
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
}
);
if (response.ok) {
const advisories = await response.json();
for (const advisory of advisories) {
processAdvisory(advisory, malwareRegistry);
}
console.log(` ✓ GitHub: Found ${Object.keys(malwareRegistry).length} malware entries`);
} else if (response.status === 403) {
console.log(' ⚠ GitHub: Rate limited (will use cached/fallback data)');
} else {
console.log(` ⚠ GitHub: Unexpected status ${response.status}`);
}
} catch (error) {
console.log(` ⚠ GitHub: Could not fetch (${error.message})`);
}
return malwareRegistry;
}
@@ -1,146 +0,0 @@
/*!
* @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.
*/
import { readdirSync, statSync } from 'node:fs';
import { join } from 'node:path';
import { spawnSync } from 'node:child_process';
const CLI_TIMEOUT_MS = 60000;
const MAX_BUFFER_SIZE = 10 * 1024 * 1024;
function isDirectory(path) {
try {
return statSync(path).isDirectory();
} catch {
return false;
}
}
function listDirectories(path) {
try {
return readdirSync(path);
} catch {
return [];
}
}
function findVSCodeExtensionCli(homeDir) {
if (!homeDir) {
return null;
}
const extensionsDir = join(homeDir, '.vscode', 'extensions');
const extensions = listDirectories(extensionsDir)
.filter(dir => /^meterian\.meterian-heidi-\d+\.\d+\.\d+$/.test(dir))
.sort()
.reverse();
for (const extension of extensions) {
const cliPath = join(extensionsDir, extension, 'packages', 'meterian-cli');
if (isDirectory(cliPath)) {
return cliPath;
}
}
return null;
}
function findGlobalCli(homeDir, rootDir) {
const possibleLocations = [
join(rootDir, 'node_modules', '@meterian', 'cli'),
homeDir ? join(homeDir, '.npm-global', 'lib', 'node_modules', '@meterian', 'cli') : null,
'/usr/local/lib/node_modules/@meterian/cli'
].filter(Boolean);
for (const location of possibleLocations) {
if (isDirectory(location)) {
return location;
}
}
return null;
}
export function findCliPath(rootDir) {
const homeDir = process.env.HOME || process.env.USERPROFILE;
return findVSCodeExtensionCli(homeDir) || findGlobalCli(homeDir, rootDir);
}
export function isAvailable(rootDir) {
return findCliPath(rootDir) !== null;
}
function formatDependenciesForCli(dependencies) {
return dependencies.map(dependency => ({
language: 'nodejs',
name: dependency.name,
version: dependency.version
}));
}
function runCliCheck(cliPath, input) {
const cliScript = join(cliPath, 'src', 'cli.js');
return spawnSync(process.execPath, [cliScript, 'check'], {
input: JSON.stringify(input),
encoding: 'utf-8',
timeout: CLI_TIMEOUT_MS,
maxBuffer: MAX_BUFFER_SIZE
});
}
function parseCliOutput(result) {
if (result.error) {
throw new Error(result.error.message);
}
if (result.status !== 0 && !result.stdout) {
throw new Error(`CLI returned status ${result.status}`);
}
return JSON.parse(result.stdout);
}
export async function checkVulnerabilities(dependencies, rootDir) {
const cliPath = findCliPath(rootDir);
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 {
const formattedInput = formatDependenciesForCli(dependencies);
const result = runCliCheck(cliPath, formattedInput);
const output = parseCliOutput(result);
console.log(` ✓ Meterian: Found ${output.vulnerable?.length || 0} vulnerable packages`);
return {
vulnerable: output.vulnerable || [],
summary: output.summary,
source: 'Meterian'
};
} catch (error) {
console.log(` ⚠ Meterian: ${error.message}`);
return { vulnerable: [], source: 'Meterian' };
}
}
-210
View File
@@ -1,210 +0,0 @@
/*!
* @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.
*/
const OSV_BATCH_API = 'https://api.osv.dev/v1/querybatch';
const REQUEST_TIMEOUT_MS = 30000;
const BATCH_SIZE = 1000;
const MALWARE_KEYWORDS = [
'malware',
'malicious',
'compromised',
'supply chain',
'backdoor',
'cryptominer',
'credential stealing',
'data exfiltration',
'typosquat'
];
function isMalwareRelated(text) {
const lowerText = text.toLowerCase();
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) {
const result = processRangeEvent(event, introduced);
introduced = result.introduced;
if (result.range) {
ranges.push(result.range);
}
}
const openEndedRange = formatOpenEndedRange(introduced);
if (openEndedRange) {
ranges.push(openEndedRange);
}
return ranges;
}
function extractVersionsFromVulnerability(vulnerability) {
const affectedEntries = vulnerability.affected || [];
const versionRanges = affectedEntries
.flatMap(entry => entry.ranges || [])
.filter(range => range.type === 'SEMVER' || range.type === 'ECOSYSTEM')
.flatMap(range => extractRangesFromEvents(range.events || []));
const explicitVersions = affectedEntries.flatMap(entry => entry.versions || []);
return {
versions: [...new Set(explicitVersions)],
versionRanges: [...new Set(versionRanges)]
};
}
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 buildQueryPayload(dependencies) {
return {
queries: dependencies.map(dependency => ({
package: { name: dependency.name, ecosystem: 'npm' },
version: dependency.version
}))
};
}
async function queryBatch(dependencies) {
const response = await fetch(OSV_BATCH_API, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(buildQueryPayload(dependencies)),
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
});
if (!response.ok) {
return [];
}
const data = await response.json();
return data.results || [];
}
function extractMalwareInfo(vulnerability) {
const summary = [vulnerability.summary || '', vulnerability.details || ''].join(' ');
if (!isMalwareRelated(summary)) {
return null;
}
const packageName = vulnerability.affected?.[0]?.package?.name;
if (!packageName) {
return null;
}
const { versions, versionRanges } = extractVersionsFromVulnerability(vulnerability);
if (versions.length === 0 && versionRanges.length === 0) {
return null;
}
return {
packageName,
versions,
versionRanges,
reason: vulnerability.summary || 'Malware detected by OSV'
};
}
function processBatchResults(batchResults, malwareRegistry) {
for (const result of batchResults) {
const vulnerabilities = result.vulns || [];
for (const vulnerability of vulnerabilities) {
const malwareInfo = extractMalwareInfo(vulnerability);
if (malwareInfo) {
if (!malwareRegistry[malwareInfo.packageName]) {
malwareRegistry[malwareInfo.packageName] = {
versions: [],
versionRanges: [],
reason: '',
source: 'OSV'
};
}
malwareRegistry[malwareInfo.packageName].versions.push(...malwareInfo.versions);
malwareRegistry[malwareInfo.packageName].versionRanges.push(...malwareInfo.versionRanges);
malwareRegistry[malwareInfo.packageName].reason = malwareInfo.reason;
}
}
}
}
export async function fetchMalwareData(projectDependencies) {
console.log(' 📡 Fetching from OSV (Google)...');
const malwareRegistry = {};
if (!projectDependencies || projectDependencies.length === 0) {
console.log(' ⚠ OSV: No dependencies to check');
return malwareRegistry;
}
try {
const dependencyBatches = splitIntoBatches(projectDependencies, BATCH_SIZE);
for (const batch of dependencyBatches) {
const batchResults = await queryBatch(batch);
processBatchResults(batchResults, malwareRegistry);
}
console.log(` ✓ OSV: Found ${Object.keys(malwareRegistry).length} malware entries`);
} catch (error) {
console.log(` ⚠ OSV: Could not fetch (${error.message})`);
}
return malwareRegistry;
}