@nuxtjp/dependency-security 0.1.2 → 0.2.1
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 +55 -0
- package/apply.mjs +3 -1
- package/checks.mjs +8 -3
- package/dependency-security.check.cjs +8 -8
- package/dependency-versions.json +8 -2
- package/package.json +2 -2
- package/project.mjs +9 -12
- package/resolved-dependency.cjs +29 -4
- package/resolved-dependency.d.cts +2 -2
- package/types/index.d.mts +3 -2
- package/version-policy.cjs +59 -0
package/README.md
CHANGED
|
@@ -55,3 +55,58 @@ against another process with the same user privileges.
|
|
|
55
55
|
[LICENSE](LICENSE) and [NOTICE](NOTICE) describe this tool's license. The unchanged
|
|
56
56
|
LICENSE-braces, LICENSE-node-forge and LICENSE-simple-git files retain upstream
|
|
57
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.
|
|
91
|
+
|
|
92
|
+
### Explicit workspace graph verification
|
|
93
|
+
|
|
94
|
+
`verifyProject(workspaceRoot, { projects })` checks the aggregate active dependency
|
|
95
|
+
graphs of explicitly selected projects, including each project's development
|
|
96
|
+
dependencies. Supply the actual owner package manager inventory (for example,
|
|
97
|
+
paths returned by `pnpm list -r --depth -1 --json`), including the workspace root
|
|
98
|
+
exactly once. The caller owns inventory completeness; this API does not parse
|
|
99
|
+
workspace YAML or infer that unselected projects were checked. More than 256
|
|
100
|
+
projects, duplicates, missing root, linked manifests, node_modules selections
|
|
101
|
+
and paths outside the workspace are refused. Dependencies shared within the
|
|
102
|
+
workspace are allowed; external resolutions and missing required dependencies
|
|
103
|
+
remain errors. Unreferenced store entries remain excluded.
|
|
104
|
+
|
|
105
|
+
The configuration CLI still refuses workspace YAML. Its owner updates that native
|
|
106
|
+
policy using the public policy export and reviewed patch assets. Graph inspection
|
|
107
|
+
does not install dependencies, invoke package lifecycles or grant runtime authority.
|
|
108
|
+
|
|
109
|
+
The reviewed policy retains fast-uri 3.1.9 for 3.x consumers and selects 4.2.3
|
|
110
|
+
for 4.x consumers; it never forces a cross-major SDK update. The 4.x correction
|
|
111
|
+
follows the upstream release: https://github.com/fastify/fast-uri/releases/tag/v4.2.3 .
|
|
112
|
+
A passing policy check is not an exhaustive advisory audit of unrelated packages.
|
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(
|
|
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
|
}
|
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,9 +4,11 @@ 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
|
}
|
|
@@ -50,18 +52,16 @@ test('RSA verifier accepts standard signatures and rejects extra or malformed Di
|
|
|
50
52
|
|
|
51
53
|
test('active dependency graph uses every required upstream security version', () => {
|
|
52
54
|
const policy = JSON.parse(fs.readFileSync(path.join(__dirname, 'dependency-versions.json'), 'utf8'));
|
|
53
|
-
|
|
54
|
-
for (const [name, version] of Object.entries(policy.overrides)) {
|
|
55
|
-
const matches = installedDependencies(process.cwd(), name, undefined, { allowMissing: true });
|
|
56
|
-
for (const manifest of matches) assert.equal(JSON.parse(fs.readFileSync(manifest, 'utf8')).version, version, name);
|
|
57
|
-
}
|
|
55
|
+
require('./version-policy.cjs').verifyInstalledPolicy(process.cwd(), policy, graphOptions);
|
|
58
56
|
});
|
|
59
57
|
|
|
60
58
|
// Preserve the ESM entry point expected by current Nuxt devtools without downgrading Git protections.
|
|
61
|
-
test('fixed simple-git retains named and legacy ESM factory exports', async
|
|
59
|
+
test('fixed simple-git retains named and legacy ESM factory exports', async context => {
|
|
62
60
|
const { pathToFileURL } = require('node:url');
|
|
63
61
|
const { installedDependencies } = require('./resolved-dependency.cjs');
|
|
64
|
-
|
|
62
|
+
const manifests = installedDependencies(process.cwd(), 'simple-git', '4.0.2', { ...graphOptions, allowMissing: true });
|
|
63
|
+
if (!manifests.length) { context.skip('No active simple-git: compatibility check is not applicable'); return; }
|
|
64
|
+
for (const manifest of manifests) {
|
|
65
65
|
const module = await import(pathToFileURL(path.join(path.dirname(manifest), 'dist/index.mjs')).href);
|
|
66
66
|
assert.equal(typeof module.simpleGit, 'function');
|
|
67
67
|
assert.equal(module.default, module.simpleGit);
|
package/dependency-versions.json
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
{
|
|
2
|
-
"schemaVersion":
|
|
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",
|
|
12
|
+
"fast-uri@4": "4.2.3"
|
|
7
13
|
},
|
|
8
14
|
"backportedAdvisories": [
|
|
9
15
|
"GHSA-vfj7-8cjw-p6xm",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nuxtjp/dependency-security",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
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",
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
"nuxt"
|
|
60
60
|
],
|
|
61
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",
|
|
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
63
|
"test": "node --test test/*.test.mjs",
|
|
64
64
|
"typecheck": "tsc --strict --noEmit --module NodeNext --moduleResolution NodeNext --target ES2022 test/public-types.mts"
|
|
65
65
|
}
|
package/project.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import { existsSync, lstatSync, readFileSync, mkdirSync, writeFileSync, renameSy
|
|
|
3
3
|
import { join, resolve } from 'node:path'
|
|
4
4
|
import { randomUUID } from 'node:crypto'
|
|
5
5
|
import resolver from './resolved-dependency.cjs'
|
|
6
|
+
import versionPolicy from './version-policy.cjs'
|
|
6
7
|
|
|
7
8
|
const patches = ['braces@3.0.3', 'node-forge@1.4.0', 'simple-git@4.0.2']
|
|
8
9
|
function regular(path) {
|
|
@@ -23,6 +24,12 @@ function object(value) {
|
|
|
23
24
|
/** Reject conflicting patches and links before mutating a project. No network access occurs. */
|
|
24
25
|
export function applySecurityPatches(project, source) {
|
|
25
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
|
+
}
|
|
26
33
|
const manifest = join(root, 'package.json')
|
|
27
34
|
const original = regular(manifest)
|
|
28
35
|
const data = parseJson(original)
|
|
@@ -33,20 +40,10 @@ export function applySecurityPatches(project, source) {
|
|
|
33
40
|
const existing = pnpm.patchedDependencies ?? {}
|
|
34
41
|
if (!object(existing)) throw new Error('Expected object-valued patchedDependencies')
|
|
35
42
|
const pinned = parseJson(regular(join(source, 'dependency-versions.json')))
|
|
36
|
-
|
|
43
|
+
const rules = versionPolicy.requirements(pinned)
|
|
37
44
|
const overrides = pnpm.overrides ?? {}
|
|
38
45
|
if (!object(overrides)) throw new Error('Expected object-valued dependency overrides')
|
|
39
|
-
|
|
40
|
-
if (!/^(?:@[a-z0-9._-]+\/)?[a-z0-9._-]+$/.test(name) || !/^\d+\.\d+\.\d+$/.test(version)) {
|
|
41
|
-
throw new Error('Exact registry package versions required')
|
|
42
|
-
}
|
|
43
|
-
for (const [selector, target] of Object.entries(overrides)) {
|
|
44
|
-
const dependency = selector.split('>').at(-1)
|
|
45
|
-
if ((dependency === name || dependency.startsWith(name + '@')) && target !== version) {
|
|
46
|
-
throw new Error('A dependency override conflicts; review it before applying')
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
}
|
|
46
|
+
versionPolicy.validateOverrides(rules, overrides)
|
|
50
47
|
const dir = join(root, '.nuxtjp-security')
|
|
51
48
|
if (existsSync(dir) && (lstatSync(dir).isSymbolicLink() || !lstatSync(dir).isDirectory())) {
|
|
52
49
|
throw new Error('Security patch directory must be a real directory')
|
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,9 +42,10 @@ 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;
|
|
48
|
+
for (const location of request.resolve.paths(name + '/package.json') ?? []) {
|
|
29
49
|
const candidate = path.join(location, name, 'package.json');
|
|
30
50
|
if (!fs.existsSync(candidate)) continue;
|
|
31
51
|
const real = fs.realpathSync(candidate);
|
|
@@ -33,11 +53,16 @@ function installedDependencies(root, target, version, options = {}) {
|
|
|
33
53
|
throw new Error('Dependency resolves outside the selected project; select the workspace root');
|
|
34
54
|
}
|
|
35
55
|
pending.push(real);
|
|
56
|
+
resolved = true;
|
|
36
57
|
break;
|
|
37
58
|
}
|
|
59
|
+
if (!resolved && Object.hasOwn({ ...data.dependencies, ...(entries.has(file) ? data.devDependencies : {}) }, name)
|
|
60
|
+
&& !Object.hasOwn(data.optionalDependencies ?? {}, name)) {
|
|
61
|
+
throw new Error('Incomplete required dependency graph');
|
|
62
|
+
}
|
|
38
63
|
}
|
|
39
64
|
}
|
|
40
65
|
if (found.size === 0 && options.allowMissing !== true) throw new Error('Dependency not present in the installed application graph');
|
|
41
66
|
return [...found].sort();
|
|
42
67
|
}
|
|
43
|
-
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,7 @@
|
|
|
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 };
|
|
@@ -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, options = {}) {
|
|
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, { ...options, 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 };
|