Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions docs/lib/content/commands/npm-approve-scripts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
---
title: npm-approve-scripts
section: 1
description: Approve install scripts for specific dependencies
---

### Synopsis

<!-- AUTOGENERATED USAGE DESCRIPTIONS -->

### Description

Manages the `allowScripts` field in your project's `package.json`, which
records which of your dependencies are permitted to run install scripts
(`preinstall`, `install`, `postinstall`, and `prepare` for non-registry
sources). This command is the recommended way to maintain that field.

In the current release, this field is advisory: install scripts still run
by default, but installs print a list of packages whose scripts have not
been reviewed. A future release will block unreviewed install scripts.

There are three modes:

```bash
npm approve-scripts <pkg> [<pkg> ...]
npm approve-scripts --all
npm approve-scripts --pending
```

`<pkg>` matches every installed version of that package. By default the
command writes pinned entries (`pkg@1.2.3`), which keep their approval
narrowed to the specific version you reviewed. Pass `--no-pin` to write
name-only entries that allow any future version.

`--all` approves every package with unreviewed install scripts in one go.

`--pending` is read-only: it lists every package whose install scripts
are not yet covered by `allowScripts`, without modifying `package.json`.

`approve-scripts` honours the asymmetric pin rule: if you re-approve a
package whose installed version has changed, the existing pin is rewritten
to track the new installed version. Multi-version statements
(`pkg@1 || 2`) are left alone, since they likely capture intent that
the command cannot infer. Existing `false` entries always win;
`approve-scripts` will not silently re-allow a package you previously
denied.

### Examples

```bash
# Approve all currently-installed install scripts after reviewing them
npm approve-scripts --all

# Approve specific packages, pinned to their installed version
npm approve-scripts canvas sharp

# Approve name-only (any version of this package is allowed)
npm approve-scripts --no-pin canvas

# Preview which packages still need review
npm approve-scripts --pending
```

### Configuration

<!-- AUTOGENERATED CONFIG DESCRIPTIONS -->

### See Also

* [npm deny-scripts](/commands/npm-deny-scripts)
* [npm install](/commands/npm-install)
* [npm rebuild](/commands/npm-rebuild)
* [package.json](/configuring-npm/package-json)
58 changes: 58 additions & 0 deletions docs/lib/content/commands/npm-deny-scripts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
---
title: npm-deny-scripts
section: 1
description: Deny install scripts for specific dependencies
---

### Synopsis

<!-- AUTOGENERATED USAGE DESCRIPTIONS -->

### Description

The companion command to [`npm approve-scripts`](/commands/npm-approve-scripts).
Writes `false` entries into the `allowScripts` field of your project's
`package.json`, recording that a dependency must not run install scripts
even if a future version would otherwise be eligible.

In the current release, install scripts still run by default, so `deny-scripts`
only affects how installs of denied packages are reported. A future release
will block unreviewed install scripts and respect deny entries at install
time.

```bash
npm deny-scripts <pkg> [<pkg> ...]
npm deny-scripts --all
```

`<pkg>` matches every installed version of that package. Denies are always
written name-only (`"pkg": false`), regardless of `--pin`. Pinning a deny
to a specific version would silently re-allow scripts for any other version
of the same package, which defeats the purpose; the command picks the
safer default for you.

`--all` denies every package with unreviewed install scripts.

If a `true` (pinned or name-only) entry exists for a package and you then
deny it, the existing allow entries are removed so the name-only deny is
unambiguous.

### Examples

```bash
# Deny a specific package outright
npm deny-scripts telemetry-pkg

# Deny everything that has install scripts and isn't already approved
npm deny-scripts --all
```

### Configuration

<!-- AUTOGENERATED CONFIG DESCRIPTIONS -->

### See Also

* [npm approve-scripts](/commands/npm-approve-scripts)
* [npm install](/commands/npm-install)
* [package.json](/configuring-npm/package-json)
6 changes: 6 additions & 0 deletions docs/lib/content/nav.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
- title: npm access
url: /commands/npm-access
description: Set access level on published packages
- title: npm approve-scripts
url: /commands/npm-approve-scripts
description: Approve install scripts for specific dependencies
- title: npm audit
url: /commands/npm-audit
description: Run a security audit
Expand All @@ -33,6 +36,9 @@
- title: npm dedupe
url: /commands/npm-dedupe
description: Reduce duplication in the package tree
- title: npm deny-scripts
url: /commands/npm-deny-scripts
description: Deny install scripts for specific dependencies
- title: npm deprecate
url: /commands/npm-deprecate
description: Deprecate a version of a package
Expand Down
13 changes: 13 additions & 0 deletions lib/commands/approve-scripts.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
const AllowScriptsCmd = require('../utils/allow-scripts-cmd.js')

class ApproveScripts extends AllowScriptsCmd {
static description = 'Approve install scripts for specific dependencies'
static name = 'approve-scripts'
static usage = ['<pkg> [<pkg> ...]', '--all', '--pending']

get verb () {
return 'approve'
}
}

module.exports = ApproveScripts
6 changes: 6 additions & 0 deletions lib/commands/ci.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const reifyFinish = require('../utils/reify-finish.js')
const resolveAllowScripts = require('../utils/resolve-allow-scripts.js')
const runScript = require('@npmcli/run-script')
const fs = require('node:fs/promises')
const path = require('node:path')
Expand All @@ -25,6 +26,9 @@ class CI extends ArboristWorkspaceCmd {
'allow-file',
'allow-git',
'allow-remote',
'allow-scripts',
'strict-script-builds',
'dangerously-allow-all-scripts',
'audit',
'bin-links',
'fund',
Expand All @@ -43,12 +47,14 @@ class CI extends ArboristWorkspaceCmd {
const ignoreScripts = this.npm.config.get('ignore-scripts')
const where = this.npm.prefix
const Arborist = require('@npmcli/arborist')
const { policy: allowScriptsPolicy } = await resolveAllowScripts(this.npm)
const opts = {
...this.npm.flatOptions,
packageLock: true, // npm ci should never skip lock files
path: where,
save: false, // npm ci should never modify the lockfile or package.json
workspaces: this.workspaceNames,
allowScripts: allowScriptsPolicy,
}

// generate an inventory from the virtual tree in the lockfile
Expand Down
13 changes: 13 additions & 0 deletions lib/commands/deny-scripts.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
const AllowScriptsCmd = require('../utils/allow-scripts-cmd.js')

class DenyScripts extends AllowScriptsCmd {
static description = 'Deny install scripts for specific dependencies'
static name = 'deny-scripts'
static usage = ['<pkg> [<pkg> ...]', '--all']

get verb () {
return 'deny'
}
}

module.exports = DenyScripts
12 changes: 12 additions & 0 deletions lib/commands/exec.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const { resolve } = require('node:path')
const libexec = require('libnpmexec')
const resolveAllowScripts = require('../utils/resolve-allow-scripts.js')
const BaseCommand = require('../base-cmd.js')

class Exec extends BaseCommand {
Expand All @@ -10,6 +11,9 @@ class Exec extends BaseCommand {
'workspace',
'workspaces',
'include-workspace-root',
'allow-scripts',
'strict-script-builds',
'dangerously-allow-all-scripts',
]

static name = 'exec'
Expand Down Expand Up @@ -74,8 +78,16 @@ class Exec extends BaseCommand {
throw this.usageError()
}

// Resolve the install-script policy from the user/global .npmrc layer
// only. The RFC requires exec/npx to ignore any project
// package.json#allowScripts; CLI flags still apply.
const { policy: allowScriptsPolicy } = await resolveAllowScripts(this.npm, {
skipProjectConfig: true,
})

return libexec({
...flatOptions,
allowScripts: allowScriptsPolicy,
// we explicitly set packageLockOnly to false because if it's true when we try to install a missing package, we won't actually install it
packageLockOnly: false,
// what the user asked to run args[0] is run by default
Expand Down
6 changes: 6 additions & 0 deletions lib/commands/install.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const runScript = require('@npmcli/run-script')
const pacote = require('pacote')
const checks = require('npm-install-checks')
const reifyFinish = require('../utils/reify-finish.js')
const resolveAllowScripts = require('../utils/resolve-allow-scripts.js')
const ArboristWorkspaceCmd = require('../arborist-cmd.js')

class Install extends ArboristWorkspaceCmd {
Expand All @@ -31,6 +32,9 @@ class Install extends ArboristWorkspaceCmd {
'allow-file',
'allow-git',
'allow-remote',
'allow-scripts',
'strict-script-builds',
'dangerously-allow-all-scripts',
'audit',
'before',
'min-release-age',
Expand Down Expand Up @@ -138,12 +142,14 @@ class Install extends ArboristWorkspaceCmd {
}

const Arborist = require('@npmcli/arborist')
const { policy: allowScriptsPolicy } = await resolveAllowScripts(this.npm)
const opts = {
...this.npm.flatOptions,
auditLevel: null,
path: where,
add: args,
workspaces: this.workspaceNames,
allowScripts: allowScriptsPolicy,
}
const arb = new Arborist(opts)
await arb.reify(opts)
Expand Down
23 changes: 22 additions & 1 deletion lib/commands/rebuild.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
const { resolve } = require('node:path')
const { output } = require('proc-log')
const { log, output } = require('proc-log')
const npa = require('npm-package-arg')
const semver = require('semver')
const ArboristWorkspaceCmd = require('../arborist-cmd.js')
const checkAllowScripts = require('../utils/check-allow-scripts.js')
const resolveAllowScripts = require('../utils/resolve-allow-scripts.js')

class Rebuild extends ArboristWorkspaceCmd {
static description = 'Rebuild a package'
Expand All @@ -12,6 +14,9 @@ class Rebuild extends ArboristWorkspaceCmd {
'bin-links',
'foreground-scripts',
'ignore-scripts',
'allow-scripts',
'strict-script-builds',
'dangerously-allow-all-scripts',
...super.params,
]

Expand All @@ -26,9 +31,11 @@ class Rebuild extends ArboristWorkspaceCmd {
const globalTop = resolve(this.npm.globalDir, '..')
const where = this.npm.global ? globalTop : this.npm.prefix
const Arborist = require('@npmcli/arborist')
const { policy: allowScriptsPolicy } = await resolveAllowScripts(this.npm)
const arb = new Arborist({
...this.npm.flatOptions,
path: where,
allowScripts: allowScriptsPolicy,
// TODO when extending ReifyCmd
// workspaces: this.workspaceNames,
})
Expand All @@ -55,6 +62,20 @@ class Rebuild extends ArboristWorkspaceCmd {
await arb.rebuild()
}

// Phase 1 advisory: list any packages whose install scripts ran (or
// would have run) and are not yet covered by allowScripts. Rebuild
// doesn't go through reifyFinish, so the walker is invoked here.
const unreviewed = await checkAllowScripts({ arb, npm: this.npm })
if (unreviewed.length > 0) {
const count = unreviewed.length
const noun = count === 1 ? 'package has' : 'packages have'
log.warn(
'rebuild',
`${count} ${noun} install scripts not yet covered by allowScripts. ` +
'Run `npm approve-scripts --pending` to review.'
)
}

output.standard('rebuilt dependencies successfully')
}

Expand Down
6 changes: 6 additions & 0 deletions lib/commands/update.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const path = require('node:path')
const { log } = require('proc-log')
const reifyFinish = require('../utils/reify-finish.js')
const resolveAllowScripts = require('../utils/resolve-allow-scripts.js')
const ArboristWorkspaceCmd = require('../arborist-cmd.js')

class Update extends ArboristWorkspaceCmd {
Expand All @@ -19,6 +20,9 @@ class Update extends ArboristWorkspaceCmd {
'package-lock',
'foreground-scripts',
'ignore-scripts',
'allow-scripts',
'strict-script-builds',
'dangerously-allow-all-scripts',
'audit',
'before',
'bin-links',
Expand Down Expand Up @@ -50,11 +54,13 @@ class Update extends ArboristWorkspaceCmd {
}

const Arborist = require('@npmcli/arborist')
const { policy: allowScriptsPolicy } = await resolveAllowScripts(this.npm)
const opts = {
...this.npm.flatOptions,
path: where,
save,
workspaces: this.workspaceNames,
allowScripts: allowScriptsPolicy,
}
const arb = new Arborist(opts)

Expand Down
Loading