@nuxtjp/dependency-security 0.1.0 → 0.2.0

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.
package/README.md CHANGED
@@ -7,7 +7,7 @@ It does not inspect arbitrary source, monitor endpoints, install dependencies,
7
7
  contact a registry or silently modify a project during installation.
8
8
 
9
9
  ```sh
10
- pnpm add -D @nuxtjp/dependency-security@0.1.0
10
+ pnpm add -D @nuxtjp/dependency-security
11
11
  pnpm exec nuxtjp-security-config --project-root . --apply
12
12
  pnpm install --no-frozen-lockfile
13
13
  pnpm install --frozen-lockfile
@@ -15,9 +15,12 @@ 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, node-forge
19
- and simple-git targets. A bare core-only library graph does not need these
20
- backports, and an absent target remains an error rather than a passing check.
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
22
+ versions and malformed/escaping graphs fail closed. A bare core-only library
23
+ graph does not need these backports.
21
24
  Use an initial install with `--ignore-scripts` before configuring a new host.
22
25
 
23
26
  Commit the generated `.nuxtjp-security` patches, manifest and lockfile. Run the
@@ -52,3 +55,36 @@ against another process with the same user privileges.
52
55
  [LICENSE](LICENSE) and [NOTICE](NOTICE) describe this tool's license. The unchanged
53
56
  LICENSE-braces, LICENSE-node-forge and LICENSE-simple-git files retain upstream
54
57
  patch-input attribution and redistribution terms.
58
+
59
+ ## Version policy 2
60
+
61
+ Exact generic fixes and major-scoped fixes have one source in
62
+ `dependency-versions.json`. Undici's maintained major lines and the current
63
+ serialize-javascript and fast-uri majors are pinned separately; configuration never forces an
64
+ unrelated major into the same implementation. Broad/conflicting caller overrides
65
+ fail before mutation. The check walks the active installed graph, accepts optional
66
+ absence only after complete traversal, rejects malformed manifests and rejects
67
+ installed majors without a reviewed rule. An absent package is not proof that its
68
+ implementation passed a vulnerability regression.
69
+
70
+ The additional upstream fixes address:
71
+ - https://github.com/advisories/GHSA-xjj6-f33q-929c
72
+ - https://github.com/advisories/GHSA-4qv8-2p9j-2685
73
+ - https://github.com/advisories/GHSA-f72v-6w45-q472
74
+ - https://github.com/advisories/GHSA-m5mm-p8h7-m29v
75
+ - https://github.com/advisories/GHSA-w74x-v6f9-j3xg
76
+ - https://github.com/advisories/GHSA-xhq7-xmqj-769h
77
+ - https://github.com/advisories/GHSA-wjr9-j795-jxcr
78
+ - https://github.com/advisories/GHSA-333x-w4r6-6crp
79
+
80
+ Apply the preset, refresh the lockfile, check the actual installed implementations,
81
+ and run the owning application's type/build/consumer acceptance. Published fixed
82
+ versions do not make Node's bundled HTTP implementation newer; Node security
83
+ updates and registry dependencies remain separate responsibilities.
84
+
85
+ Configuration writes target a single-project package.json. If a selected root
86
+ already has pnpm-workspace.yaml, the CLI reports an explicit unsupported-workspace
87
+ result before mutation; its owner must project the reviewed rules into that YAML
88
+ rather than create competing configuration in package.json. Graph verification
89
+ still supports an explicitly selected workspace root. No workspace YAML parser or
90
+ second package-manager authority is introduced.
package/apply.mjs CHANGED
@@ -13,7 +13,9 @@ if (args.length !== 3 || args[0] !== '--project-root' || args[2] !== '--apply')
13
13
  applySecurityPatches(args[1], dirname(fileURLToPath(import.meta.url)))
14
14
  console.log('Reviewed backports configured. Run pnpm install --no-frozen-lockfile to refresh the lockfile.')
15
15
  } catch (error) {
16
- console.error('Selected project configuration could not be applied; review manifest and patch conflicts.')
16
+ console.error(error?.code === 'UNSUPPORTED_WORKSPACE_CONFIGURATION'
17
+ ? 'Workspace policy requires its owner-native YAML update; no configuration changed.'
18
+ : 'Selected project configuration could not be applied; review manifest and patch conflicts.')
17
19
  process.exitCode = 1
18
20
  }
19
21
  }
@@ -50,19 +50,16 @@ test('RSA verifier accepts standard signatures and rejects extra or malformed Di
50
50
 
51
51
  test('active dependency graph uses every required upstream security version', () => {
52
52
  const policy = JSON.parse(fs.readFileSync(path.join(__dirname, 'dependency-versions.json'), 'utf8'));
53
- const { installedDependencies } = require('./resolved-dependency.cjs');
54
- for (const [name, version] of Object.entries(policy.overrides)) {
55
- const matches = installedDependencies(process.cwd(), name);
56
- assert.ok(matches.length > 0, 'Required dependency is absent: ' + name);
57
- for (const manifest of matches) assert.equal(JSON.parse(fs.readFileSync(manifest, 'utf8')).version, version, name);
58
- }
53
+ require('./version-policy.cjs').verifyInstalledPolicy(process.cwd(), policy);
59
54
  });
60
55
 
61
56
  // Preserve the ESM entry point expected by current Nuxt devtools without downgrading Git protections.
62
- test('fixed simple-git retains named and legacy ESM factory exports', async () => {
57
+ test('fixed simple-git retains named and legacy ESM factory exports', async context => {
63
58
  const { pathToFileURL } = require('node:url');
64
59
  const { installedDependencies } = require('./resolved-dependency.cjs');
65
- for (const manifest of installedDependencies(process.cwd(), 'simple-git', '4.0.2')) {
60
+ const manifests = installedDependencies(process.cwd(), 'simple-git', '4.0.2', { allowMissing: true });
61
+ if (!manifests.length) { context.skip('No active simple-git: compatibility check is not applicable'); return; }
62
+ for (const manifest of manifests) {
66
63
  const module = await import(pathToFileURL(path.join(path.dirname(manifest), 'dist/index.mjs')).href);
67
64
  assert.equal(typeof module.simpleGit, 'function');
68
65
  assert.equal(module.default, module.simpleGit);
@@ -1,9 +1,14 @@
1
1
  {
2
- "schemaVersion": 1,
2
+ "schemaVersion": 2,
3
3
  "overrides": {
4
4
  "simple-git": "4.0.2",
5
5
  "@simple-git/argv-parser": "2.0.1",
6
- "esbuild": "0.28.2"
6
+ "esbuild": "0.28.2",
7
+ "undici@6": "6.29.1",
8
+ "undici@7": "7.30.1",
9
+ "undici@8": "8.12.2",
10
+ "serialize-javascript@7": "7.1.3",
11
+ "fast-uri@3": "3.1.9"
7
12
  },
8
13
  "backportedAdvisories": [
9
14
  "GHSA-vfj7-8cjw-p6xm",
package/package.json CHANGED
@@ -1,13 +1,12 @@
1
1
  {
2
2
  "name": "@nuxtjp/dependency-security",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
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",
7
7
  "engines": {
8
8
  "node": ">=22.19.0"
9
9
  },
10
- "packageManager": "pnpm@10.29.3",
11
10
  "repository": {
12
11
  "type": "git",
13
12
  "url": "git+https://github.com/nuxtjp/nuxtjp-ui.git",
@@ -48,13 +47,20 @@
48
47
  "nuxtjp-security-config": "./apply.mjs",
49
48
  "nuxtjp-security-check": "./check.mjs"
50
49
  },
51
- "scripts": {
52
- "build": "node --check index.mjs && node --check project.mjs && node --check apply.mjs && node --check check.mjs && node --check resolved-dependency.cjs",
53
- "test": "node --test test/*.test.mjs",
54
- "typecheck": "tsc --strict --noEmit --module NodeNext --moduleResolution NodeNext --target ES2022 test/public-types.mts"
55
- },
56
50
  "devDependencies": {
57
51
  "typescript": "5.9.3",
58
52
  "@types/node": "24.12.0"
53
+ },
54
+ "keywords": [
55
+ "dependency-security",
56
+ "supply-chain-security",
57
+ "pnpm",
58
+ "security",
59
+ "nuxt"
60
+ ],
61
+ "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
+ "test": "node --test test/*.test.mjs",
64
+ "typecheck": "tsc --strict --noEmit --module NodeNext --moduleResolution NodeNext --target ES2022 test/public-types.mts"
59
65
  }
60
- }
66
+ }
package/project.mjs CHANGED
@@ -2,6 +2,8 @@
2
2
  import { existsSync, lstatSync, readFileSync, mkdirSync, writeFileSync, renameSync, unlinkSync, realpathSync } from 'node:fs'
3
3
  import { join, resolve } from 'node:path'
4
4
  import { randomUUID } from 'node:crypto'
5
+ import resolver from './resolved-dependency.cjs'
6
+ import versionPolicy from './version-policy.cjs'
5
7
 
6
8
  const patches = ['braces@3.0.3', 'node-forge@1.4.0', 'simple-git@4.0.2']
7
9
  function regular(path) {
@@ -22,6 +24,12 @@ function object(value) {
22
24
  /** Reject conflicting patches and links before mutating a project. No network access occurs. */
23
25
  export function applySecurityPatches(project, source) {
24
26
  const root = realpathSync(resolve(project))
27
+ if (existsSync(join(root, 'pnpm-workspace.yaml'))) {
28
+ regular(join(root, 'pnpm-workspace.yaml'))
29
+ const error = new Error('Use the owner-native workspace configuration; no second manifest policy was written')
30
+ error.code = 'UNSUPPORTED_WORKSPACE_CONFIGURATION'
31
+ throw error
32
+ }
25
33
  const manifest = join(root, 'package.json')
26
34
  const original = regular(manifest)
27
35
  const data = parseJson(original)
@@ -32,24 +40,15 @@ export function applySecurityPatches(project, source) {
32
40
  const existing = pnpm.patchedDependencies ?? {}
33
41
  if (!object(existing)) throw new Error('Expected object-valued patchedDependencies')
34
42
  const pinned = parseJson(regular(join(source, 'dependency-versions.json')))
35
- if (pinned.schemaVersion !== 1 || !object(pinned.overrides)) throw new Error('Invalid dependency version policy')
43
+ const rules = versionPolicy.requirements(pinned)
36
44
  const overrides = pnpm.overrides ?? {}
37
45
  if (!object(overrides)) throw new Error('Expected object-valued dependency overrides')
38
- for (const [name, version] of Object.entries(pinned.overrides)) {
39
- if (!/^(?:@[a-z0-9._-]+\/)?[a-z0-9._-]+$/.test(name) || !/^\d+\.\d+\.\d+$/.test(version)) {
40
- throw new Error('Exact registry package versions required')
41
- }
42
- for (const [selector, target] of Object.entries(overrides)) {
43
- const dependency = selector.split('>').at(-1)
44
- if ((dependency === name || dependency.startsWith(name + '@')) && target !== version) {
45
- throw new Error('A dependency override conflicts; review it before applying')
46
- }
47
- }
48
- }
46
+ versionPolicy.validateOverrides(rules, overrides)
49
47
  const dir = join(root, '.nuxtjp-security')
50
48
  if (existsSync(dir) && (lstatSync(dir).isSymbolicLink() || !lstatSync(dir).isDirectory())) {
51
49
  throw new Error('Security patch directory must be a real directory')
52
50
  }
51
+ const activeSimpleGit = resolver.installedDependencies(root, 'simple-git', undefined, { allowMissing: true }).length > 0
53
52
  const inputs = patches.map(name => {
54
53
  const relative = `.nuxtjp-security/${name.replace('@', '-')}.patch`
55
54
  if (existing[name] !== undefined && existing[name] !== relative) {
@@ -60,11 +59,15 @@ export function applySecurityPatches(project, source) {
60
59
  if (existsSync(target) && !regular(target).equals(bytes)) {
61
60
  throw new Error('An existing patch differs; no files were overwritten')
62
61
  }
63
- return { name, relative, bytes, target }
62
+ return { name, relative, bytes, target, required: name !== 'simple-git@4.0.2' || activeSimpleGit }
64
63
  })
65
64
  if (!readFileSync(manifest).equals(original)) throw new Error('Manifest changed during preparation')
66
65
  mkdirSync(dir, { recursive: true })
67
66
  for (const patch of inputs) {
67
+ if (!patch.required) {
68
+ delete existing[patch.name]
69
+ continue
70
+ }
68
71
  if (!existsSync(patch.target)) writeFileSync(patch.target, patch.bytes, { flag: 'wx' })
69
72
  existing[patch.name] = patch.relative
70
73
  }
@@ -3,7 +3,7 @@ const fs = require('node:fs');
3
3
  const path = require('node:path');
4
4
  const { createRequire } = require('node:module');
5
5
 
6
- function installedDependencies(root, target, version) {
6
+ function installedDependencies(root, target, version, options = {}) {
7
7
  root = fs.realpathSync(root);
8
8
  const pending = [path.join(root, 'package.json')];
9
9
  const seen = new Set();
@@ -25,7 +25,8 @@ function installedDependencies(root, target, version) {
25
25
  const names = Object.keys({ ...data.dependencies, ...data.optionalDependencies, ...data.peerDependencies,
26
26
  ...(file === path.join(root, 'package.json') ? data.devDependencies : {}) });
27
27
  for (const name of names) {
28
- for (const location of request.resolve.paths(name) ?? []) {
28
+ let resolved = false;
29
+ for (const location of request.resolve.paths(name + '/package.json') ?? []) {
29
30
  const candidate = path.join(location, name, 'package.json');
30
31
  if (!fs.existsSync(candidate)) continue;
31
32
  const real = fs.realpathSync(candidate);
@@ -33,11 +34,16 @@ function installedDependencies(root, target, version) {
33
34
  throw new Error('Dependency resolves outside the selected project; select the workspace root');
34
35
  }
35
36
  pending.push(real);
37
+ resolved = true;
36
38
  break;
37
39
  }
40
+ if (!resolved && Object.hasOwn(data.dependencies ?? {}, name)
41
+ && !Object.hasOwn(data.optionalDependencies ?? {}, name)) {
42
+ throw new Error('Incomplete required dependency graph');
43
+ }
38
44
  }
39
45
  }
40
- if (found.size === 0) throw new Error('Dependency not present in the installed application graph');
46
+ if (found.size === 0 && options.allowMissing !== true) throw new Error('Dependency not present in the installed application graph');
41
47
  return [...found].sort();
42
48
  }
43
49
  module.exports = { installedDependencies };
@@ -1,2 +1,2 @@
1
1
  /** Traverse only the selected installed application graph, with a finite budget. */
2
- export function installedDependencies(root: string, target: string, version?: string): string[];
2
+ export function installedDependencies(root: string, target: string, version?: string, options?: { allowMissing?: boolean }): string[];
@@ -0,0 +1,59 @@
1
+ /** Closed exact-version rules; scoped fixes never coerce an unrelated major. */
2
+ const fs = require('node:fs');
3
+ const { installedDependencies } = require('./resolved-dependency.cjs');
4
+ const packageSelector = /^((?:@[a-z0-9._-]+\/)?[a-z0-9._-]+)(?:@(0|[1-9][0-9]{0,3}))?$/;
5
+ const versionPattern = /^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$/;
6
+ function selection(selector) {
7
+ const match = packageSelector.exec(selector);
8
+ if (!match) throw new Error('Unreviewed dependency selector');
9
+ return { name: match[1], major: match[2] };
10
+ }
11
+ function requirements(policy) {
12
+ if (!policy || policy.schemaVersion !== 2 || !policy.overrides || Array.isArray(policy.overrides)
13
+ || typeof policy.overrides !== 'object') throw new Error('Invalid dependency policy');
14
+ return Object.entries(policy.overrides).map(([selector, version]) => {
15
+ const rule = selection(selector);
16
+ if (typeof version !== 'string' || !versionPattern.test(version)
17
+ || (rule.major !== undefined && rule.major !== version.split('.')[0])) throw new Error('Invalid fixed version');
18
+ return { ...rule, selector, version };
19
+ });
20
+ }
21
+ function validateOverrides(rules, overrides) {
22
+ for (const [selector, target] of Object.entries(overrides)) {
23
+ const tail = selector.split('>').at(-1);
24
+ for (const rule of rules) {
25
+ if (tail !== rule.name && !tail.startsWith(rule.name + '@')) continue;
26
+ let existing;
27
+ try { existing = selection(tail); }
28
+ catch { throw new Error('A dependency override conflicts with the reviewed selector policy'); }
29
+ if (rule.major !== undefined) {
30
+ if (existing.major === undefined) throw new Error('A broad dependency override conflicts with major-scoped fixes');
31
+ if (existing.major !== rule.major) continue;
32
+ }
33
+ if (target !== rule.version) throw new Error('A dependency override conflicts; review it before applying');
34
+ }
35
+ }
36
+ }
37
+ function checkVersions(policy, observed) {
38
+ const rules = requirements(policy);
39
+ for (const [name, versions] of Object.entries(observed)) {
40
+ const selected = rules.filter(rule => rule.name === name);
41
+ for (const version of versions) {
42
+ if (typeof version !== 'string' || !versionPattern.test(version)) throw new Error('Invalid installed version');
43
+ const applicable = selected.filter(rule => rule.major === undefined || rule.major === version.split('.')[0]);
44
+ if (applicable.length !== 1 || applicable[0].version !== version) {
45
+ throw new Error('Installed dependency version is outside the reviewed security policy');
46
+ }
47
+ }
48
+ }
49
+ }
50
+ function verifyInstalledPolicy(root, policy) {
51
+ const names = new Set(requirements(policy).map(rule => rule.name)), observed = {};
52
+ for (const name of names) {
53
+ observed[name] = installedDependencies(root, name, undefined, { allowMissing: true })
54
+ .map(file => JSON.parse(fs.readFileSync(file, 'utf8')).version);
55
+ }
56
+ checkVersions(policy, observed);
57
+ return observed;
58
+ }
59
+ module.exports = { requirements, validateOverrides, checkVersions, verifyInstalledPolicy };