@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.
- package/LICENSE-deepmerge +21 -0
- package/NOTICE +3 -1
- package/README.md +46 -9
- package/advisories.mjs +42 -0
- package/checks.mjs +8 -3
- package/dependency-security.check.cjs +32 -5
- package/dependency-versions.json +4 -2
- package/index.mjs +1 -0
- package/package.json +3 -2
- package/patches/deepmerge-4.3.1.patch +78 -0
- package/project.mjs +11 -2
- package/resolved-dependency.cjs +23 -4
- package/resolved-dependency.d.cts +2 -2
- package/types/index.d.mts +7 -2
- package/version-policy.cjs +2 -2
|
@@ -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
|
-
|
|
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
|
|
19
|
-
|
|
20
|
-
active.
|
|
21
|
-
|
|
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
|
|
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
|
-
|
|
34
|
-
|
|
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
|
|
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
|
-
|
|
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:
|
|
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
|
|
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);
|
package/dependency-versions.json
CHANGED
|
@@ -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.
|
|
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:
|
|
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 })
|
package/resolved-dependency.cjs
CHANGED
|
@@ -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
|
|
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
|
-
...(
|
|
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
|
|
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
|
|
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;
|
package/version-policy.cjs
CHANGED
|
@@ -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);
|