@nuxtjp/dependency-security 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2012 James Halliday, Josh Duff, and other contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
package/NOTICE CHANGED
@@ -1,4 +1,6 @@
1
1
  NuxtJP dependency-security
2
2
  Apache-2.0 applies to the configuration and verification tool.
3
3
  Third-party patch inputs retain the included LICENSE-braces, LICENSE-node-forge
4
- and LICENSE-simple-git notices; these do not relicense the upstream packages.
4
+ LICENSE-simple-git and LICENSE-deepmerge notices; these do not relicense the upstream packages.
5
+ The deepmerge 4.3.1 backport rejects prototype-sensitive keys in both inputs;
6
+ see https://github.com/TehShrike/deepmerge/issues/273 and CVE-2026-93753.
package/README.md CHANGED
@@ -15,10 +15,10 @@ pnpm exec nuxtjp-security-check --project-root .
15
15
  pnpm audit --json
16
16
  ```
17
17
 
18
- The preset requires an installed Nuxt host with the selected braces and node-forge
19
- targets. The simple-git compatibility patch applies only when that dependency is
20
- active. Newer DevTools no longer needs it; absence is not a reason to install an
21
- unused Git process library. Present targets still require the reviewed fixed
18
+ The preset requires an installed Nuxt host with the selected braces target. The
19
+ node-forge and simple-git patches apply only when their exact reviewed targets are
20
+ active. Node-forge 1.4.2 uses its upstream implementation and is checked by the
21
+ same RSA regressions; 1.4.0 still needs the backport. Absence is not a reason to install an unused package. Present targets still require the reviewed fixed
22
22
  versions and malformed/escaping graphs fail closed. A bare core-only library
23
23
  graph does not need these backports.
24
24
  Use an initial install with `--ignore-scripts` before configuring a new host.
@@ -26,12 +26,21 @@ Use an initial install with `--ignore-scripts` before configuring a new host.
26
26
  Commit the generated `.nuxtjp-security` patches, manifest and lockfile. Run the
27
27
  check after every locked install. Configuration refuses conflicting patches,
28
28
  symlink storage and concurrent manifest changes; unrelated settings are retained.
29
- The check loads the explicitly installed braces/node-forge implementations and
30
- exercises bounded recursion and malformed RSA signature cases. It creates a
29
+ The check loads the explicitly installed braces implementation and, when present,
30
+ node-forge, and exercises bounded recursion and malformed RSA signature cases. It creates a
31
31
  never-issued ephemeral test key in memory and never reads an account key.
32
32
 
33
- Two upstream advisories currently have no fixed registry version:
34
- GHSA-vfj7-8cjw-p6xm (braces 3.0.3) and GHSA-86w9-cpqp-85rv (node-forge 1.4.0).
33
+ When deepmerge 4.3.1 is present, the reviewed backport rejects `__proto__`,
34
+ `constructor`, and `prototype` keys in both merge inputs. This deliberately
35
+ excludes those keys from merged output. Regression tests check both distributed
36
+ entry points against poisoned prototypes and ordinary nested and array merges.
37
+ The reported issue and proposed remediation are documented at
38
+ https://github.com/TehShrike/deepmerge/issues/273. The patch is omitted when
39
+ deepmerge is absent, and a different installed version requires review.
40
+
41
+ The following upstream advisories have no recorded fixed registry version:
42
+ GHSA-vfj7-8cjw-p6xm (braces 3.0.3), GHSA-86w9-cpqp-85rv (node-forge 1.4.0)
43
+ and GHSA-gcqx-w48m-w2h4 (deepmerge 4.3.1).
35
44
  Version-only audit findings remain visible. Tested patches supplement those
36
45
  releases; every other advisory blocks validation. Exact fixed override versions
37
46
  for simple-git, its argument parser and esbuild live in `dependency-versions.json`.
@@ -47,13 +56,19 @@ applySecurityPatches('/explicit/application')
47
56
  const active = installedDependencies('/explicit/application', 'braces', '3.0.3')
48
57
  ```
49
58
 
59
+ `verifyAuditedAdvisories(project, ids)` rejects unrelated advisories and reported
60
+ backports with no active target, then runs the installed dependency regressions.
61
+ This binds version-only audit exceptions to the implementations actually tested.
62
+ `verifyAuditReport(project, report)` additionally checks every severity total,
63
+ rejects unaccounted findings and preserves zero critical/moderate thresholds.
64
+
50
65
  A missing target or an installed dependency resolving outside the selected project
51
66
  is an error. The API does not execute installs or inspected application scripts.
52
67
  The caller must own and explicitly select the project; this is not an OS sandbox
53
68
  against another process with the same user privileges.
54
69
 
55
70
  [LICENSE](LICENSE) and [NOTICE](NOTICE) describe this tool's license. The unchanged
56
- LICENSE-braces, LICENSE-node-forge and LICENSE-simple-git files retain upstream
71
+ LICENSE-braces, LICENSE-node-forge, LICENSE-simple-git and LICENSE-deepmerge files retain upstream
57
72
  patch-input attribution and redistribution terms.
58
73
 
59
74
  ## Version policy 2
@@ -88,3 +103,25 @@ result before mutation; its owner must project the reviewed rules into that YAML
88
103
  rather than create competing configuration in package.json. Graph verification
89
104
  still supports an explicitly selected workspace root. No workspace YAML parser or
90
105
  second package-manager authority is introduced.
106
+
107
+ ### Explicit workspace graph verification
108
+
109
+ `verifyProject(workspaceRoot, { projects })` checks the aggregate active dependency
110
+ graphs of explicitly selected projects, including each project's development
111
+ dependencies. Supply the actual owner package manager inventory (for example,
112
+ paths returned by `pnpm list -r --depth -1 --json`), including the workspace root
113
+ exactly once. The caller owns inventory completeness; this API does not parse
114
+ workspace YAML or infer that unselected projects were checked. More than 256
115
+ projects, duplicates, missing root, linked manifests, node_modules selections
116
+ and paths outside the workspace are refused. Dependencies shared within the
117
+ workspace are allowed; external resolutions and missing required dependencies
118
+ remain errors. Unreferenced store entries remain excluded.
119
+
120
+ The configuration CLI still refuses workspace YAML. Its owner updates that native
121
+ policy using the public policy export and reviewed patch assets. Graph inspection
122
+ does not install dependencies, invoke package lifecycles or grant runtime authority.
123
+
124
+ The reviewed policy retains fast-uri 3.1.9 for 3.x consumers and selects 4.2.3
125
+ for 4.x consumers; it never forces a cross-major SDK update. The 4.x correction
126
+ follows the upstream release: https://github.com/fastify/fast-uri/releases/tag/v4.2.3 .
127
+ A passing policy check is not an exhaustive advisory audit of unrelated packages.
package/advisories.mjs ADDED
@@ -0,0 +1,42 @@
1
+ /** Bind accepted version-only audit findings to actual tested dependency targets. */
2
+ import { readFileSync } from 'node:fs'
3
+ import { verifyProject } from './checks.mjs'
4
+ import resolver from './resolved-dependency.cjs'
5
+
6
+ const targets = Object.freeze({
7
+ 'GHSA-vfj7-8cjw-p6xm': 'braces',
8
+ 'GHSA-86w9-cpqp-85rv': 'node-forge',
9
+ 'GHSA-gcqx-w48m-w2h4': 'deepmerge',
10
+ })
11
+
12
+ export function verifyAuditedAdvisories(project, advisories, options = {}) {
13
+ if (!Array.isArray(advisories)) throw new Error('Expected an advisory identifier array')
14
+ const policy = JSON.parse(readFileSync(new URL('./dependency-versions.json', import.meta.url)))
15
+ for (const id of advisories) {
16
+ const target = targets[id]
17
+ if (!target || !policy.backportedAdvisories.includes(id)) throw new Error(`Unremediated advisory: ${id}`)
18
+ if (!resolver.installedDependencies(project, target, undefined, { ...options, allowMissing: true }).length) {
19
+ throw new Error(`Reported advisory has no active regression target: ${id}`)
20
+ }
21
+ }
22
+ const result = verifyProject(project, options)
23
+ if (result.exitCode !== 0) throw new Error('Installed dependency regressions failed')
24
+ }
25
+
26
+ /** Verify audit totals as well as each advisory against the tested active graph. */
27
+ export function verifyAuditReport(project, report, options = {}) {
28
+ if (!report || typeof report !== 'object' || !report.advisories || typeof report.advisories !== 'object') {
29
+ throw new Error('Invalid audit report')
30
+ }
31
+ const findings = Object.values(report.advisories)
32
+ const totals = report.metadata?.vulnerabilities
33
+ for (const severity of ['info', 'low', 'moderate', 'high', 'critical']) {
34
+ const total = totals?.[severity]
35
+ if (!Number.isSafeInteger(total) || total < 0) throw new Error(`Invalid ${severity} audit total`)
36
+ if (['critical', 'moderate'].includes(severity) && total !== 0) throw new Error(`Unexpected ${severity} vulnerabilities`)
37
+ if (total > findings.filter(item => item?.severity === severity).length) {
38
+ throw new Error(`Unaccounted ${severity} vulnerabilities`)
39
+ }
40
+ }
41
+ verifyAuditedAdvisories(project, findings.map(item => item?.github_advisory_id), options)
42
+ }
package/checks.mjs CHANGED
@@ -2,12 +2,17 @@
2
2
  import { spawnSync } from 'node:child_process'
3
3
  import { fileURLToPath } from 'node:url'
4
4
  import { realpathSync } from 'node:fs'
5
- export function verifyProject(project) {
5
+ import { dirname } from 'node:path'
6
+ import resolver from './resolved-dependency.cjs'
7
+ export function verifyProject(project, options = {}) {
8
+ const root = realpathSync(project)
9
+ const projects = resolver.projectManifests(root, options.projects).map(dirname)
6
10
  const env = { ...process.env }
7
11
  // A parent's node:test context would silently skip a child suite.
8
- for (const name of ['NODE_TEST_CONTEXT', 'NODE_OPTIONS', 'NODE_PATH']) delete env[name]
12
+ for (const name of ['NODE_TEST_CONTEXT', 'NODE_OPTIONS', 'NODE_PATH', 'NUXTJP_SECURITY_PROJECTS']) delete env[name]
13
+ env.NUXTJP_SECURITY_PROJECTS = JSON.stringify(projects)
9
14
  const result = spawnSync(process.execPath,
10
15
  ['--test', fileURLToPath(new URL('./dependency-security.check.cjs', import.meta.url))],
11
- { cwd: realpathSync(project), env, encoding: 'utf8', timeout: 120000, maxBuffer: 4 * 1024 * 1024 })
16
+ { cwd: root, env, encoding: 'utf8', timeout: 120000, maxBuffer: 4 * 1024 * 1024 })
12
17
  return { exitCode: result.status ?? 2, stdout: result.stdout ?? '', stderr: result.stderr ?? '' }
13
18
  }
@@ -4,14 +4,17 @@ const { createRequire } = require('node:module');
4
4
  const { generateKeyPairSync, sign, privateEncrypt, constants } = require('node:crypto');
5
5
  const path = require('node:path');
6
6
  const fs = require('node:fs');
7
+ const graphOptions = process.env.NUXTJP_SECURITY_PROJECTS
8
+ ? { projects: JSON.parse(process.env.NUXTJP_SECURITY_PROJECTS) } : {};
7
9
  function dependency(name, version) {
8
10
  const { installedDependencies } = require('./resolved-dependency.cjs');
9
- const matches = installedDependencies(process.cwd(), name, version);
11
+ const matches = installedDependencies(process.cwd(), name, version, graphOptions);
10
12
  assert.equal(matches.length, 1, 'Expected one active locked dependency implementation');
11
13
  return createRequire(matches[0])(name);
12
14
  }
13
15
  const braces = dependency('braces', '3.0.3');
14
- const forge = dependency('node-forge', '1.4.0');
16
+ const forgeManifests = require('./resolved-dependency.cjs').installedDependencies(process.cwd(), 'node-forge', undefined, { ...graphOptions, allowMissing: true });
17
+ const deepmergeManifests = require('./resolved-dependency.cjs').installedDependencies(process.cwd(), 'deepmerge', '4.3.1', { ...graphOptions, allowMissing: true });
15
18
  test('brace compilation, expansion and stringification retain ordinary behavior', () => {
16
19
  assert.equal(braces.compile('src/{a,b}.vue'), 'src/(a|b).vue');
17
20
  assert.deepEqual(braces.expand('src/{a,b}.vue'), ['src/a.vue', 'src/b.vue']);
@@ -28,7 +31,11 @@ test('deep brace, parenthesis and caller AST inputs reject before stack exhausti
28
31
  const cyclic = { type: 'root', nodes: [] }; cyclic.nodes.push(cyclic);
29
32
  assert.throws(() => braces.compile(cyclic), SyntaxError);
30
33
  });
31
- test('RSA verifier accepts standard signatures and rejects extra or malformed DigestAlgorithm elements', () => {
34
+ test('RSA verifier accepts standard signatures and rejects extra or malformed DigestAlgorithm elements', context => {
35
+ if (!forgeManifests.length) { context.skip('No active node-forge: RSA regression is not applicable'); return; }
36
+ for (const manifest of forgeManifests) {
37
+ assert.ok(['1.4.0', '1.4.2'].includes(JSON.parse(fs.readFileSync(manifest, 'utf8')).version), 'Unreviewed node-forge version');
38
+ const forge = createRequire(manifest)('node-forge');
32
39
  const keys = generateKeyPairSync('rsa', { modulusLength: 2048 });
33
40
  const publicKey = forge.pki.publicKeyFromPem(keys.publicKey.export({ type: 'spki', format: 'pem' }));
34
41
  const data = Buffer.from('never-issued security regression fixture');
@@ -46,18 +53,38 @@ test('RSA verifier accepts standard signatures and rejects extra or malformed Di
46
53
  try { accepted = publicKey.verify(digest, signature); } catch { /* Rejection is the expected result for a malformed signature. */ }
47
54
  assert.equal(accepted, false, 'Malformed algorithm must not verify');
48
55
  }
56
+ }
49
57
  });
50
58
 
51
59
  test('active dependency graph uses every required upstream security version', () => {
52
60
  const policy = JSON.parse(fs.readFileSync(path.join(__dirname, 'dependency-versions.json'), 'utf8'));
53
- require('./version-policy.cjs').verifyInstalledPolicy(process.cwd(), policy);
61
+ require('./version-policy.cjs').verifyInstalledPolicy(process.cwd(), policy, graphOptions);
62
+ });
63
+
64
+ test('deepmerge rejects prototype keys in both inputs and preserves ordinary merges', context => {
65
+ if (!deepmergeManifests.length) { context.skip('No active deepmerge: prototype regression is not applicable'); return; }
66
+ for (const manifest of deepmergeManifests) {
67
+ for (const entry of ['dist/cjs.js', 'dist/umd.js']) {
68
+ const merge = require(path.join(path.dirname(manifest), entry));
69
+ const malicious = JSON.parse('{"session":{"__proto__":{"admin":true}},"__proto__":{"admin":true}}');
70
+ for (const target of [{ session: null }, Object.create(null), JSON.parse('{"__proto__":{"admin":true}}')]) {
71
+ const result = merge(target, malicious);
72
+ assert.equal(result.admin, undefined);
73
+ assert.equal(result.session?.admin, undefined);
74
+ assert.equal(Object.getPrototypeOf(result), Object.prototype);
75
+ assert.equal(Object.getPrototypeOf(result.session), Object.prototype);
76
+ }
77
+ const result = merge({ nested: { a: 1 }, list: [1] }, { nested: { b: 2 }, list: [2] });
78
+ assert.deepEqual(result, { nested: { a: 1, b: 2 }, list: [1, 2] });
79
+ }
80
+ }
54
81
  });
55
82
 
56
83
  // Preserve the ESM entry point expected by current Nuxt devtools without downgrading Git protections.
57
84
  test('fixed simple-git retains named and legacy ESM factory exports', async context => {
58
85
  const { pathToFileURL } = require('node:url');
59
86
  const { installedDependencies } = require('./resolved-dependency.cjs');
60
- const manifests = installedDependencies(process.cwd(), 'simple-git', '4.0.2', { allowMissing: true });
87
+ const manifests = installedDependencies(process.cwd(), 'simple-git', '4.0.2', { ...graphOptions, allowMissing: true });
61
88
  if (!manifests.length) { context.skip('No active simple-git: compatibility check is not applicable'); return; }
62
89
  for (const manifest of manifests) {
63
90
  const module = await import(pathToFileURL(path.join(path.dirname(manifest), 'dist/index.mjs')).href);
@@ -8,10 +8,12 @@
8
8
  "undici@7": "7.30.1",
9
9
  "undici@8": "8.12.2",
10
10
  "serialize-javascript@7": "7.1.3",
11
- "fast-uri@3": "3.1.9"
11
+ "fast-uri@3": "3.1.9",
12
+ "fast-uri@4": "4.2.3"
12
13
  },
13
14
  "backportedAdvisories": [
14
15
  "GHSA-vfj7-8cjw-p6xm",
15
- "GHSA-86w9-cpqp-85rv"
16
+ "GHSA-86w9-cpqp-85rv",
17
+ "GHSA-gcqx-w48m-w2h4"
16
18
  ]
17
19
  }
package/index.mjs CHANGED
@@ -5,3 +5,4 @@ import { applySecurityPatches as configure } from './project.mjs'
5
5
  export { installedDependencies } from './resolved-dependency.cjs'
6
6
  export function applySecurityPatches(project) { configure(project, dirname(fileURLToPath(import.meta.url))) }
7
7
  export { verifyProject } from './checks.mjs'
8
+ export { verifyAuditedAdvisories, verifyAuditReport } from './advisories.mjs'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nuxtjp/dependency-security",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "Explicit pnpm backport configuration and active dependency regression checks without Nuxt peers",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -28,6 +28,7 @@
28
28
  "LICENSE-braces",
29
29
  "LICENSE-node-forge",
30
30
  "LICENSE-simple-git",
31
+ "LICENSE-deepmerge",
31
32
  "README.md"
32
33
  ],
33
34
  "exports": {
@@ -59,7 +60,7 @@
59
60
  "nuxt"
60
61
  ],
61
62
  "scripts": {
62
- "build": "node --check index.mjs && node --check project.mjs && node --check apply.mjs && node --check check.mjs && node --check resolved-dependency.cjs && node --check version-policy.cjs",
63
+ "build": "node --check index.mjs && node --check project.mjs && node --check apply.mjs && node --check check.mjs && node --check advisories.mjs && node --check resolved-dependency.cjs && node --check version-policy.cjs",
63
64
  "test": "node --test test/*.test.mjs",
64
65
  "typecheck": "tsc --strict --noEmit --module NodeNext --moduleResolution NodeNext --target ES2022 test/public-types.mts"
65
66
  }
@@ -0,0 +1,78 @@
1
+ diff --git a/index.js b/index.js
2
+ index 77968ae..ea40fd2 100644
3
+ --- a/index.js
4
+ +++ b/index.js
5
+ @@ -51,15 +51,20 @@ function propertyIsUnsafe(target, key) {
6
+ && Object.propertyIsEnumerable.call(target, key)) // and also unsafe if they're nonenumerable.
7
+ }
8
+
9
+ +function keyIsForbidden(key) {
10
+ + return key === '__proto__' || key === 'constructor' || key === 'prototype'
11
+ +}
12
+ +
13
+ function mergeObject(target, source, options) {
14
+ var destination = {}
15
+ if (options.isMergeableObject(target)) {
16
+ getKeys(target).forEach(function(key) {
17
+ + if (keyIsForbidden(key)) return
18
+ destination[key] = cloneUnlessOtherwiseSpecified(target[key], options)
19
+ })
20
+ }
21
+ getKeys(source).forEach(function(key) {
22
+ - if (propertyIsUnsafe(target, key)) {
23
+ + if (keyIsForbidden(key) || propertyIsUnsafe(target, key)) {
24
+ return
25
+ }
26
+
27
+ diff --git a/dist/cjs.js b/dist/cjs.js
28
+ index 7c36cbd..3f743f1 100644
29
+ --- a/dist/cjs.js
30
+ +++ b/dist/cjs.js
31
+ @@ -76,15 +76,20 @@ function propertyIsUnsafe(target, key) {
32
+ && Object.propertyIsEnumerable.call(target, key)) // and also unsafe if they're nonenumerable.
33
+ }
34
+
35
+ +function keyIsForbidden(key) {
36
+ + return key === '__proto__' || key === 'constructor' || key === 'prototype'
37
+ +}
38
+ +
39
+ function mergeObject(target, source, options) {
40
+ var destination = {};
41
+ if (options.isMergeableObject(target)) {
42
+ getKeys(target).forEach(function(key) {
43
+ + if (keyIsForbidden(key)) return
44
+ destination[key] = cloneUnlessOtherwiseSpecified(target[key], options);
45
+ });
46
+ }
47
+ getKeys(source).forEach(function(key) {
48
+ - if (propertyIsUnsafe(target, key)) {
49
+ + if (keyIsForbidden(key) || propertyIsUnsafe(target, key)) {
50
+ return
51
+ }
52
+
53
+ diff --git a/dist/umd.js b/dist/umd.js
54
+ index 4071e7c..6a6ab99 100644
55
+ --- a/dist/umd.js
56
+ +++ b/dist/umd.js
57
+ @@ -80,15 +80,20 @@
58
+ && Object.propertyIsEnumerable.call(target, key)) // and also unsafe if they're nonenumerable.
59
+ }
60
+
61
+ + function keyIsForbidden(key) {
62
+ + return key === '__proto__' || key === 'constructor' || key === 'prototype'
63
+ + }
64
+ +
65
+ function mergeObject(target, source, options) {
66
+ var destination = {};
67
+ if (options.isMergeableObject(target)) {
68
+ getKeys(target).forEach(function(key) {
69
+ + if (keyIsForbidden(key)) return
70
+ destination[key] = cloneUnlessOtherwiseSpecified(target[key], options);
71
+ });
72
+ }
73
+ getKeys(source).forEach(function(key) {
74
+ - if (propertyIsUnsafe(target, key)) {
75
+ + if (keyIsForbidden(key) || propertyIsUnsafe(target, key)) {
76
+ return
77
+ }
78
+
package/project.mjs CHANGED
@@ -5,7 +5,7 @@ import { randomUUID } from 'node:crypto'
5
5
  import resolver from './resolved-dependency.cjs'
6
6
  import versionPolicy from './version-policy.cjs'
7
7
 
8
- const patches = ['braces@3.0.3', 'node-forge@1.4.0', 'simple-git@4.0.2']
8
+ const patches = ['braces@3.0.3', 'node-forge@1.4.0', 'simple-git@4.0.2', 'deepmerge@4.3.1']
9
9
  function regular(path) {
10
10
  const stat = lstatSync(path)
11
11
  if (stat.isSymbolicLink() || !stat.isFile() || stat.size > 1024 * 1024) {
@@ -49,6 +49,13 @@ export function applySecurityPatches(project, source) {
49
49
  throw new Error('Security patch directory must be a real directory')
50
50
  }
51
51
  const activeSimpleGit = resolver.installedDependencies(root, 'simple-git', undefined, { allowMissing: true }).length > 0
52
+ const forge = resolver.installedDependencies(root, 'node-forge', undefined, { allowMissing: true })
53
+ const forgeVersions = forge.map(path => parseJson(regular(path)).version)
54
+ if (forgeVersions.some(version => !['1.4.0', '1.4.2'].includes(version))) {
55
+ throw new Error('Dependency version changed; review its security patch')
56
+ }
57
+ const activeForge = forgeVersions.includes('1.4.0')
58
+ const activeDeepmerge = resolver.installedDependencies(root, 'deepmerge', '4.3.1', { allowMissing: true }).length > 0
52
59
  const inputs = patches.map(name => {
53
60
  const relative = `.nuxtjp-security/${name.replace('@', '-')}.patch`
54
61
  if (existing[name] !== undefined && existing[name] !== relative) {
@@ -59,7 +66,9 @@ export function applySecurityPatches(project, source) {
59
66
  if (existsSync(target) && !regular(target).equals(bytes)) {
60
67
  throw new Error('An existing patch differs; no files were overwritten')
61
68
  }
62
- return { name, relative, bytes, target, required: name !== 'simple-git@4.0.2' || activeSimpleGit }
69
+ return { name, relative, bytes, target, required:
70
+ (name !== 'simple-git@4.0.2' || activeSimpleGit) && (name !== 'node-forge@1.4.0' || activeForge) &&
71
+ (name !== 'deepmerge@4.3.1' || activeDeepmerge) }
63
72
  })
64
73
  if (!readFileSync(manifest).equals(original)) throw new Error('Manifest changed during preparation')
65
74
  mkdirSync(dir, { recursive: true })
@@ -3,9 +3,28 @@ const fs = require('node:fs');
3
3
  const path = require('node:path');
4
4
  const { createRequire } = require('node:module');
5
5
 
6
+ function projectManifests(root, projects = [root]) {
7
+ root = fs.realpathSync(root);
8
+ if (!Array.isArray(projects) || projects.length === 0 || projects.length > 256) throw new Error('Invalid selected project inventory');
9
+ const manifests = projects.map(project => {
10
+ if (typeof project !== 'string') throw new Error('Invalid selected project');
11
+ const selected = fs.realpathSync(path.resolve(root, project));
12
+ const relative = path.relative(root, selected);
13
+ if (relative === '..' || relative.startsWith('..' + path.sep) || path.isAbsolute(relative)
14
+ || relative.split(path.sep).includes('node_modules')) throw new Error('Selected project escapes workspace boundary');
15
+ const manifest = path.join(selected, 'package.json');
16
+ if (fs.realpathSync(manifest) !== manifest) throw new Error('Linked project manifest refused');
17
+ return manifest;
18
+ });
19
+ if (new Set(manifests).size !== manifests.length || !manifests.includes(path.join(root, 'package.json'))) {
20
+ throw new Error('Project inventory must include the workspace root exactly once');
21
+ }
22
+ return manifests;
23
+ }
6
24
  function installedDependencies(root, target, version, options = {}) {
7
25
  root = fs.realpathSync(root);
8
- const pending = [path.join(root, 'package.json')];
26
+ const entries = new Set(projectManifests(root, options.projects));
27
+ const pending = [...entries];
9
28
  const seen = new Set();
10
29
  const found = new Set();
11
30
  while (pending.length) {
@@ -23,7 +42,7 @@ function installedDependencies(root, target, version, options = {}) {
23
42
  }
24
43
  const request = createRequire(file);
25
44
  const names = Object.keys({ ...data.dependencies, ...data.optionalDependencies, ...data.peerDependencies,
26
- ...(file === path.join(root, 'package.json') ? data.devDependencies : {}) });
45
+ ...(entries.has(file) ? data.devDependencies : {}) });
27
46
  for (const name of names) {
28
47
  let resolved = false;
29
48
  for (const location of request.resolve.paths(name + '/package.json') ?? []) {
@@ -37,7 +56,7 @@ function installedDependencies(root, target, version, options = {}) {
37
56
  resolved = true;
38
57
  break;
39
58
  }
40
- if (!resolved && Object.hasOwn(data.dependencies ?? {}, name)
59
+ if (!resolved && Object.hasOwn({ ...data.dependencies, ...(entries.has(file) ? data.devDependencies : {}) }, name)
41
60
  && !Object.hasOwn(data.optionalDependencies ?? {}, name)) {
42
61
  throw new Error('Incomplete required dependency graph');
43
62
  }
@@ -46,4 +65,4 @@ function installedDependencies(root, target, version, options = {}) {
46
65
  if (found.size === 0 && options.allowMissing !== true) throw new Error('Dependency not present in the installed application graph');
47
66
  return [...found].sort();
48
67
  }
49
- module.exports = { installedDependencies };
68
+ module.exports = { installedDependencies, projectManifests };
@@ -1,2 +1,2 @@
1
- /** Traverse only the selected installed application graph, with a finite budget. */
2
- export function installedDependencies(root: string, target: string, version?: string, options?: { allowMissing?: boolean }): string[];
1
+ /** Traverse active graphs within the selected workspace boundary. Projects must include its root once. */
2
+ export function installedDependencies(root: string, target: string, version?: string, options?: { allowMissing?: boolean; projects?: readonly string[] }): string[];
package/types/index.d.mts CHANGED
@@ -1,6 +1,11 @@
1
+ export interface GraphOptions { projects?: readonly string[] }
1
2
  /** Configure reviewed pnpm backports. No installation or network access occurs. */
2
3
  export function applySecurityPatches(project: string): void;
3
4
  /** Resolve exact active packages; reject external-workspace resolution and missing targets. */
4
- export function installedDependencies(root: string, target: string, version?: string): string[];
5
+ export function installedDependencies(root: string, target: string, version?: string, options?: GraphOptions & { allowMissing?: boolean }): string[];
5
6
  /** Run the known dependency regressions, returning bounded local output and exit status. */
6
- export function verifyProject(project: string): { exitCode: number; stdout: string; stderr: string };
7
+ export function verifyProject(project: string, options?: GraphOptions): { exitCode: number; stdout: string; stderr: string };
8
+ /** Accept only reported advisories with installed targets whose regressions pass. */
9
+ export function verifyAuditedAdvisories(project: string, advisories: string[], options?: GraphOptions): void;
10
+ /** Check audit totals and bind each advisory to an active, regression-tested target. */
11
+ export function verifyAuditReport(project: string, report: unknown, options?: GraphOptions): void;
@@ -47,10 +47,10 @@ function checkVersions(policy, observed) {
47
47
  }
48
48
  }
49
49
  }
50
- function verifyInstalledPolicy(root, policy) {
50
+ function verifyInstalledPolicy(root, policy, options = {}) {
51
51
  const names = new Set(requirements(policy).map(rule => rule.name)), observed = {};
52
52
  for (const name of names) {
53
- observed[name] = installedDependencies(root, name, undefined, { allowMissing: true })
53
+ observed[name] = installedDependencies(root, name, undefined, { ...options, allowMissing: true })
54
54
  .map(file => JSON.parse(fs.readFileSync(file, 'utf8')).version);
55
55
  }
56
56
  checkVersions(policy, observed);