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
5 changes: 4 additions & 1 deletion lib/utils/check-allow-scripts.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,16 @@ const { collectUnreviewedScripts } = require('@npmcli/arborist/lib/unreviewed-sc
// `includeWhenIgnored` keeps listing unreviewed packages even when
// ignore-scripts is set, so approve/deny can show what you'd move from a
// blanket ignore-scripts to an allowlist. Execution callers leave it false.
const checkAllowScripts = async ({ arb, npm, tree, includeWhenIgnored = false }) =>
const checkAllowScripts = async ({
arb, npm, tree, includeWhenIgnored = false, pruneExtraneous = false,
}) =>
collectUnreviewedScripts({
tree: tree || arb.actualTree,
policy: arb.options?.allowScripts || null,
ignoreScripts: !!arb.options?.ignoreScripts,
dangerouslyAllowAllScripts: !!npm?.flatOptions?.dangerouslyAllowAllScripts,
includeWhenIgnored,
pruneExtraneous,
})

module.exports = checkAllowScripts
4 changes: 3 additions & 1 deletion lib/utils/strict-allow-scripts-preflight.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@ const strictAllowScriptsPreflight = async ({ arb, npm, idealTreeOpts }) => {
tree = arb.actualTree
}

const unreviewed = await checkAllowScripts({ arb, npm, tree })
const unreviewed = await checkAllowScripts({
arb, npm, tree, pruneExtraneous: idealTreeOpts !== undefined && arb.options?.prune !== false,
})
if (unreviewed.length === 0) {
return
}
Expand Down
13 changes: 13 additions & 0 deletions test/lib/commands/install-scripts.js
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,19 @@ t.test('install-scripts ls with no unreviewed says so', async t => {
t.match(joinedOutput(), /No packages with unreviewed install scripts/)
})

t.test('install-scripts ls includes extraneous actual-tree packages', async t => {
const prefixDir = setupProject({ withScripts: ['orphan'] })
const pkg = JSON.parse(prefixDir['package.json'])
pkg.dependencies = {}
prefixDir['package.json'] = JSON.stringify(pkg)
const lock = JSON.parse(prefixDir['package-lock.json'])
lock.packages[''] = pkg
prefixDir['package-lock.json'] = JSON.stringify(lock)
const { npm, joinedOutput } = await mockNpm(t, { prefixDir })
await npm.exec('install-scripts', ['ls'])
t.match(joinedOutput(), /orphan@1\.0\.0/)
})

t.test('install-scripts ls rejects positional args', async t => {
const { npm } = await mockNpm(t, {
prefixDir: setupProject({ withScripts: ['canvas'] }),
Expand Down
32 changes: 32 additions & 0 deletions test/lib/utils/strict-allow-scripts-preflight.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const node = ({
name = 'pkg',
version = '1.0.0',
scripts = { install: 'node-gyp rebuild' },
extraneous = false,
} = {}) => ({
name,
resolved: `https://registry.npmjs.org/${name}/-/${name}-${version}.tgz`,
Expand All @@ -17,6 +18,7 @@ const node = ({
isProjectRoot: false,
isWorkspace: false,
isLink: false,
extraneous,
package: { name, version, scripts },
})

Expand Down Expand Up @@ -89,6 +91,20 @@ t.test('passes when the only unreviewed node is inert (platform-incompatible opt
t.pass('no error thrown for inert node')
})

t.test('passes when the only unreviewed node is an extraneous orphan', async t => {
// Lockfile-only workspace orphans are pruned before scripts run and must not
// make the command-level strict preflight reject an install (npm/cli#9680).
const orphan = node({ name: 'core-js', extraneous: true })
orphan.isRegistryDependency = false
const arb = makeArb({ ideal: tree([orphan]) })
await preflight({
arb,
npm: { flatOptions: { strictAllowScripts: true } },
idealTreeOpts: {},
})
t.pass('no error thrown for extraneous orphan')
})

t.test('passes when all install-script nodes are explicitly approved', async t => {
const arb = makeArb({
ideal: tree([node({ name: 'canvas' })]),
Expand Down Expand Up @@ -232,3 +248,19 @@ t.test('global error points at --allow-scripts, not approve-scripts', async t =>
}
)
})

t.test('rebuild reviews extraneous actual-tree nodes', async t => {
const arb = makeArb({ actual: tree([node({ name: 'orphan', extraneous: true })]) })
await t.rejects(preflight({ arb, npm: { flatOptions: { strictAllowScripts: true } } }), {
code: 'ESTRICTALLOWSCRIPTS',
message: /orphan@1\.0\.0/,
})
})

t.test('reify without pruning reviews extraneous nodes', async t => {
const arb = makeArb({ ideal: tree([node({ name: 'orphan', extraneous: true })]) })
arb.options.prune = false
await t.rejects(preflight({
arb, npm: { flatOptions: { strictAllowScripts: true } }, idealTreeOpts: {},
}), { code: 'ESTRICTALLOWSCRIPTS' })
})
6 changes: 6 additions & 0 deletions workspaces/arborist/lib/unreviewed-scripts.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const collectUnreviewedScripts = async ({
ignoreScripts = false,
dangerouslyAllowAllScripts = false,
includeWhenIgnored = false,
pruneExtraneous = false,
} = {}) => {
// With ignore-scripts set, no scripts run, so execution callers bail out
// here. approve/deny pass includeWhenIgnored so they keep listing
Expand Down Expand Up @@ -57,6 +58,11 @@ const collectUnreviewedScripts = async ({
// must not be flagged (npm/cli#9562).
continue
}
if (pruneExtraneous && node.extraneous) {
// Only a pruning reify removes orphans before scripts run. Actual-tree
// listing and rebuild must still review their scripts (npm/cli#9680).
continue
}

const verdict = isScriptAllowed(node, resolvedPolicy)
if (verdict === true || verdict === false) {
Expand Down
35 changes: 35 additions & 0 deletions workspaces/arborist/test/unreviewed-scripts.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ const node = ({
isLink = false,
inBundle = false,
inert = false,
extraneous = false,
resolved,
} = {}) => ({
name,
Expand All @@ -41,6 +42,7 @@ const node = ({
isLink,
inBundle,
inert,
extraneous,
isRegistryDependency: true,
package: { name, version, scripts },
})
Expand Down Expand Up @@ -92,6 +94,39 @@ t.test('collectUnreviewedScripts', async t => {
t.strictSame(result, [])
})

t.test('skips extraneous (orphan) registry nodes when pruning', async t => {
// An extraneous orphan is pruned before reify runs any install script, so it must not gate the install even when the policy neither allows nor denies it (npm/cli#9680).
const result = await collectUnreviewedScripts({
tree: tree([
node({ name: 'orphan', scripts: { install: 'x' }, extraneous: true }),
]),
pruneExtraneous: true,
policy: null,
})
t.strictSame(result, [])
})

t.test('skips extraneous nodes even when policy denies by name', async t => {
// Same orphan, but with a name-only deny entry.
// An extraneous registry node usually has no resolved URL for the matcher to verify, so the deny misses and it falls through to "unreviewed".
// It still must not gate the install (npm/cli#9680).
const orphan = node({ name: 'esbuild', scripts: { install: 'x' }, extraneous: true })
orphan.isRegistryDependency = false
const result = await collectUnreviewedScripts({
tree: tree([orphan]),
policy: { esbuild: false },
pruneExtraneous: true,
})
t.strictSame(result, [])
})

t.test('reviews extraneous nodes in the actual tree', async t => {
const orphan = node({ name: 'orphan', scripts: { install: 'x' }, extraneous: true })
t.strictSame(await collectUnreviewedScripts({ tree: tree([orphan]) }), [
{ node: orphan, scripts: { install: 'x' } },
])
})

t.test('skips nodes with no install-relevant scripts', async t => {
const result = await collectUnreviewedScripts({
tree: tree([node({ scripts: { test: 'jest' } })]),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const strictAllowScriptsPreflight = async (arb, opts) => {

const unreviewed = await collectUnreviewedScripts({
tree: arb.idealTree,
pruneExtraneous: opts.prune !== false,
policy: opts.allowScripts || null,
ignoreScripts: opts.ignoreScripts,
dangerouslyAllowAllScripts: opts.dangerouslyAllowAllScripts,
Expand Down