Comparing version 0.0.2 to 0.0.3
@@ -20,2 +20,3 @@ export async function analyzePackages(packages) { | ||
return { | ||
packageName: packageInfo.packageJson.name, | ||
packageInfo, | ||
@@ -22,0 +23,0 @@ messages |
@@ -1,14 +0,60 @@ | ||
import { lockfile, packageJson } from "./index.js"; | ||
import yargs from "yargs"; | ||
import { hideBin } from "yargs/helpers"; | ||
import path from "node:path"; | ||
import { analyzeLockfile, analyzePackageJson } from "./index.js"; | ||
import { writeToConsole } from "./formatter.js"; | ||
let result; | ||
if (process.argv[2] && process.argv[2] === "package-json") { | ||
result = await packageJson({ cwd: process.cwd() }); | ||
} | ||
else { | ||
result = await lockfile({ cwd: process.cwd() }); | ||
} | ||
writeToConsole(result); | ||
if (result.numberOfFindings) { | ||
process.exit(1); | ||
} | ||
await yargs(hideBin(process.argv)) | ||
.parserConfiguration({ | ||
"parse-numbers": false | ||
}) | ||
.scriptName("no-scripts") | ||
.command({ | ||
command: "$0 [projectDir]", | ||
desc: "Check lockfile", | ||
builder: (yargs) => { | ||
yargs | ||
.positional("projectDir", { | ||
describe: "Project directory to scan. Defaults to the current working directory", | ||
type: "string", | ||
}); | ||
}, | ||
handler: async (argv) => { | ||
const cwd = argv.projectDir ? path.resolve(argv.projectDir) : process.cwd(); | ||
const lockfileResults = await analyzeLockfile({ cwd, ignorePackages: argv.ignore || [] }); | ||
let packageJsonResult; | ||
if (argv.includeLocal) { | ||
console.log(""); | ||
packageJsonResult = await analyzePackageJson({ cwd, ignorePackages: argv.ignore || [] }); | ||
} | ||
console.log(""); | ||
writeToConsole(lockfileResults, packageJsonResult); | ||
if (lockfileResults.numberOfFindings || (packageJsonResult && packageJsonResult.numberOfFindings)) { | ||
process.exit(1); | ||
} | ||
} | ||
}) | ||
.option("ignore", { | ||
describe: "Package names to ignore", | ||
array: true, | ||
string: true, | ||
}) | ||
.option("include-local", { | ||
describe: "Extend check to include local dependencies", | ||
boolean: true, | ||
}) | ||
.showHelpOnFail(true) | ||
.strict(true) | ||
.fail(function (msg, err) { | ||
if (err) { | ||
console.log(`Analysis Failed:`); | ||
console.log(err.message); | ||
console.log(""); | ||
console.log(err.stack); | ||
} | ||
else { | ||
// Yargs parsing error | ||
console.log(`Command failed: ${msg}`); | ||
} | ||
}) | ||
.parse(); | ||
//# sourceMappingURL=cli.js.map |
@@ -1,3 +0,3 @@ | ||
export function writeToConsole(results) { | ||
for (const packageResult of results.packages) { | ||
export function writeToConsole(lockfileResults, packageJsonResults) { | ||
for (const packageResult of lockfileResults.packages) { | ||
if (packageResult.messages.length) { | ||
@@ -11,4 +11,19 @@ console.log(`Findings for package ${packageResult.packageInfo.packageJson.name}:`); | ||
} | ||
console.log(`Findings: ${results.numberOfFindings}`); | ||
console.log(`${lockfileResults.numberOfFindings} Findings`); | ||
if (packageJsonResults) { | ||
console.log(""); | ||
console.log("------------------------"); | ||
console.log("Local results:"); | ||
for (const packageResult of packageJsonResults.packages) { | ||
if (packageResult.messages.length) { | ||
console.log(`Findings for package ${packageResult.packageInfo.packageJson.name}:`); | ||
for (const msg of packageResult.messages) { | ||
console.log(` ${msg}`); | ||
} | ||
console.log(""); | ||
} | ||
} | ||
console.log(`${packageJsonResults.numberOfFindings} Findings`); | ||
} | ||
} | ||
//# sourceMappingURL=formatter.js.map |
@@ -10,8 +10,10 @@ import path from "node:path"; | ||
*/ | ||
export async function lockfile({ cwd }) { | ||
export async function analyzeLockfile({ cwd, ignorePackages }) { | ||
const rootPackageInfo = await getRootPackage(cwd); | ||
let lockfile = await tryReadJson(path.join(rootPackageInfo.modulePath, `package-lock.json`)); | ||
let packageLock = true; | ||
if (!lockfile) { | ||
// If no package-lock.json is available, try npm-shrinkwrap.json | ||
lockfile = await tryReadJson(path.join(rootPackageInfo.modulePath, `npm-shrinkwrap.json`)); | ||
packageLock = false; | ||
} | ||
@@ -23,11 +25,13 @@ if (!lockfile) { | ||
} | ||
console.log(`Analyzing using lockfile and remote registry`); | ||
console.log(`Analyzing using ${packageLock ? "package-lock.json" : "npm-shrinkwrap.json"} and registry`); | ||
const packages = await getPackagesFromLockfile(rootPackageInfo, lockfile); | ||
removeIgnoredPackages(packages, ignorePackages); | ||
return await analyzePackages(packages); | ||
} | ||
export async function packageJson({ cwd }) { | ||
console.log(`Analyzing using locally installed dependencies`); | ||
export async function analyzePackageJson({ cwd, ignorePackages }) { | ||
console.log(`Analyzing using package.json and locally installed dependencies`); | ||
const rootPackageInfo = await getRootPackage(cwd); | ||
const depPackages = await getPackagesFromInstalledDependencies(rootPackageInfo); | ||
return await analyzePackages(depPackages); | ||
const packages = await getPackagesFromInstalledDependencies(rootPackageInfo); | ||
removeIgnoredPackages(packages, ignorePackages); | ||
return await analyzePackages(packages); | ||
} | ||
@@ -64,2 +68,19 @@ async function getRootPackage(cwd) { | ||
} | ||
function removeIgnoredPackages(packages, ignorePackages) { | ||
const foundIgnoredPackages = new Set(); | ||
if (ignorePackages && ignorePackages.length) { | ||
for (const [modulePath, packageInfo] of packages.entries()) { | ||
if (ignorePackages.includes(packageInfo.packageJson.name)) { | ||
packages.delete(modulePath); | ||
foundIgnoredPackages.add(packageInfo.packageJson.name); | ||
} | ||
} | ||
ignorePackages.forEach((ignoredPackageName) => { | ||
if (!foundIgnoredPackages.has(ignoredPackageName)) { | ||
throw new Error(`Failed to find ignored package: ${ignoredPackageName}`); | ||
} | ||
}); | ||
} | ||
console.log(`(ignoring ${foundIgnoredPackages.size} packages)`); | ||
} | ||
//# sourceMappingURL=index.js.map |
@@ -10,10 +10,22 @@ import { promisify } from "node:util"; | ||
} | ||
function addDepsToArray(targetArray, depsToAdd, optional) { | ||
for (const packageName of depsToAdd) { | ||
targetArray.push({ | ||
packageName, | ||
optional | ||
}); | ||
} | ||
} | ||
function getPackageDependencies(pkg, rootPkg = false) { | ||
const deps = Object.keys(pkg.dependencies || {}); | ||
if (pkg.optionalDependencies) { | ||
deps.push(...Object.keys(pkg.optionalDependencies)); | ||
const deps = []; | ||
addDepsToArray(deps, Object.keys(pkg.dependencies || {}), false); | ||
addDepsToArray(deps, Object.keys(pkg.optionalDependencies || {}), true); | ||
// TODO: Maybe add check whether peerDependenciesMeta flags it as optional | ||
addDepsToArray(deps, Object.keys(pkg.peerDependencies || {}), true); | ||
addDepsToArray(deps, Object.keys(pkg.bundleDependencies || {}), false); | ||
addDepsToArray(deps, Object.keys(pkg.bundledDependencies || {}), false); | ||
if (rootPkg) { | ||
// Ignore devDependencies unless for the root package | ||
addDepsToArray(deps, Object.keys(pkg.devDependencies || {}), false); | ||
} | ||
if (rootPkg && pkg.devDependencies) { | ||
deps.push(...Object.keys(pkg.devDependencies)); | ||
} | ||
return deps; | ||
@@ -31,3 +43,3 @@ } | ||
packageJson: await readPackageJson(modulePath), | ||
modulePath: modulePath | ||
modulePath | ||
}; | ||
@@ -48,17 +60,27 @@ } | ||
const depPackages = new Map(); | ||
async function collectDependencies(moduleName, parentPath) { | ||
const res = await getPackageJson(moduleName, parentPath); | ||
if (depPackages.has(res.modulePath)) { | ||
// Deps already processed | ||
return; | ||
async function collectDependencies(moduleName, parentPath, optional = false) { | ||
try { | ||
const res = await getPackageJson(moduleName, parentPath); | ||
if (depPackages.has(res.modulePath)) { | ||
// Deps already processed | ||
return; | ||
} | ||
depPackages.set(res.modulePath, res); | ||
await Promise.all(getPackageDependencies(res.packageJson).map(({ packageName, optional }) => { | ||
return collectDependencies(packageName, res.modulePath, optional); | ||
})); | ||
} | ||
depPackages.set(res.modulePath, res); | ||
return await Promise.all(getPackageDependencies(res.packageJson).map((depName) => { | ||
return collectDependencies(depName, res.modulePath); | ||
})); | ||
catch (err) { | ||
if (optional) { | ||
// Do nothing | ||
} | ||
else { | ||
throw err; | ||
} | ||
} | ||
} | ||
const rootDeps = getPackageDependencies(rootPackageInfo.packageJson, true); | ||
const rootModulePath = path.dirname(rootPackageInfo.modulePath); | ||
await Promise.all(rootDeps.map((depName) => { | ||
return collectDependencies(depName, rootModulePath); | ||
const rootModulePath = rootPackageInfo.modulePath; | ||
await Promise.all(rootDeps.map(({ packageName, optional }) => { | ||
return collectDependencies(packageName, rootModulePath, optional); | ||
})); | ||
@@ -65,0 +87,0 @@ return depPackages; |
@@ -56,6 +56,9 @@ import path from "node:path"; | ||
} | ||
if (packageDescriptor.link) { | ||
// Ignore links | ||
return; | ||
} | ||
if (!packageDescriptor.resolved) { | ||
// Packages from other sources than an npm registry (i.e. local) | ||
console.log(packageDescriptor); | ||
throw new Error(`Failed to resolve lockfile entry for package: ${packageLocation}`); | ||
return; | ||
} | ||
@@ -62,0 +65,0 @@ if (requests.has(packageDescriptor.resolved)) { |
{ | ||
"name": "no-scripts", | ||
"version": "0.0.2", | ||
"version": "0.0.3", | ||
"description": "", | ||
@@ -9,4 +9,5 @@ "type": "module", | ||
"build-watch": "tsc -w -p .", | ||
"test": "npm run lint", | ||
"lint": "eslint ." | ||
"test": "npm run lint && npm run check", | ||
"lint": "eslint .", | ||
"check": "node --import tsx/esm src/cli.ts --ignore esbuild" | ||
}, | ||
@@ -33,3 +34,4 @@ "keywords": [], | ||
"resolve": "^1.22.8", | ||
"rimraf": "^5.0.5" | ||
"rimraf": "^5.0.5", | ||
"yargs": "^17.7.2" | ||
}, | ||
@@ -36,0 +38,0 @@ "devDependencies": { |
@@ -15,16 +15,22 @@ # no-scripts | ||
## Usage | ||
There are two modes of operation, "Lockfile Mode" and "package.json Mode". | ||
### Lockfile Mode | ||
```sh | ||
no-scripts lockfile | ||
no-scripts | ||
``` | ||
This mode will start by fetching the tarballs of all dependencies listed in the project's lockfile from the corresponding npm registry. It then evaluates the package.json files of every package, applying normalization using [`@npmcli/package-json`](https://www.npmjs.com/package/@npmcli/package-json) in order to detect implicit install-scripts such as `node-gyp rebuild` which would be executed if a package contains a certain file. | ||
By default, no-scripts will start by fetching the tarballs of all dependencies listed in the project's [lockfile](https://docs.npmjs.com/cli/v10/configuring-npm/package-lock-json) from the corresponding npm registry. It then evaluates the `package.json` files of every package, applying normalization using [`@npmcli/package-json`](https://www.npmjs.com/package/@npmcli/package-json) in order to detect implicit install-scripts such as `node-gyp rebuild` which would be executed if a package contains `*.gyp` files. | ||
### package.json Mode | ||
**Note:** Local dependencies which are referenced via links or workspaces are not analyzed. You can use the [`--include-local`](#include-local-dependencies) option to additionally check them. | ||
### Ignore Dependencies | ||
```sh | ||
no-scripts package-json | ||
no-scripts --ignore esbuild | ||
``` | ||
### Include Local Dependencies | ||
```sh | ||
no-scripts --include-local | ||
``` | ||
This mode operates fully offline and is therefore rather fast. However, since a malicious package that has already been installed *could* have altered it's own package.json during the `postinstall` phase, this mode might be fooled into thinking that a package has no scripts even though they were already executed. I have not yet evaluated the feasability of such an attack though. | ||
@@ -31,0 +37,0 @@ |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
164739
4344
40
6
+ Addedyargs@^17.7.2
+ Addedcliui@8.0.1(transitive)
+ Addedescalade@3.1.1(transitive)
+ Addedget-caller-file@2.0.5(transitive)
+ Addedrequire-directory@2.1.1(transitive)
+ Addedy18n@5.0.8(transitive)
+ Addedyargs@17.7.2(transitive)
+ Addedyargs-parser@21.1.1(transitive)