@dependably/npm-check 1.7.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/LICENSE +201 -0
- package/README.md +71 -0
- package/bin/cli.js +1577 -0
- package/package.json +103 -0
- package/src/audit-config.js +331 -0
- package/src/audit.js +805 -0
- package/src/backup.js +235 -0
- package/src/checker.js +771 -0
- package/src/checksum-fixer.js +419 -0
- package/src/deprecation.js +325 -0
- package/src/fixer.js +207 -0
- package/src/format-library.js +132 -0
- package/src/index.js +138 -0
- package/src/integrity.js +519 -0
- package/src/migrator.js +263 -0
- package/src/npmrc-validator.js +205 -0
- package/src/overrides.js +71 -0
- package/src/package-json-validator.js +356 -0
- package/src/parallel-processor.js +374 -0
- package/src/parser.js +133 -0
- package/src/performance.js +283 -0
- package/src/pinner.js +248 -0
- package/src/pnpm-format.js +229 -0
- package/src/pnpm-workspace-validator.js +121 -0
- package/src/progress-reporter.js +245 -0
- package/src/pruner.js +177 -0
- package/src/remediate.js +389 -0
- package/src/report.js +663 -0
- package/src/schema.js +76 -0
- package/src/streaming-parser.js +251 -0
- package/src/updater.js +251 -0
- package/src/usage-scanner.js +215 -0
- package/src/validator.js +282 -0
- package/src/vuln.js +618 -0
- package/src/workers/dedupe-worker.js +20 -0
- package/src/workers/hash-upgrade-worker.js +20 -0
- package/src/workers/migration-worker.js +21 -0
- package/src/workers/validation-worker.js +20 -0
package/package.json
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@dependably/npm-check",
|
|
3
|
+
"version": "1.7.0",
|
|
4
|
+
"description": "A comprehensive tool for validating, migrating, and updating npm package-lock.json files across versions 1, 2, and 3.",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/index.js",
|
|
9
|
+
"./package.json": "./package.json"
|
|
10
|
+
},
|
|
11
|
+
"bin": {
|
|
12
|
+
"npm-check": "bin/cli.js"
|
|
13
|
+
},
|
|
14
|
+
"publishConfig": {
|
|
15
|
+
"access": "public"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"src/",
|
|
19
|
+
"bin/",
|
|
20
|
+
"README.md",
|
|
21
|
+
"LICENSE"
|
|
22
|
+
],
|
|
23
|
+
"scripts": {
|
|
24
|
+
"setup:hooks": "git config core.hooksPath .githooks",
|
|
25
|
+
"test": "node --experimental-vm-modules ./node_modules/jest/bin/jest.js --config=jest.config.mjs",
|
|
26
|
+
"test:unit": "npm test -- --testPathPattern=tests/unit",
|
|
27
|
+
"test:integration": "INTEGRATION_TESTS=1 npm test -- --testPathPattern=tests/integration --runInBand",
|
|
28
|
+
"test:all": "npm run test:unit && npm run test:integration",
|
|
29
|
+
"test:coverage": "npm test -- --coverage",
|
|
30
|
+
"docker:build": "docker-compose -f tests/integration/docker/docker-compose.yml build",
|
|
31
|
+
"docker:test:node22": "docker-compose -f tests/integration/docker/docker-compose.yml run --rm test-node22-npm10",
|
|
32
|
+
"docker:test:node24": "docker-compose -f tests/integration/docker/docker-compose.yml run --rm test-node24-npm11",
|
|
33
|
+
"docker:test:all": "docker-compose -f tests/integration/docker/docker-compose.yml up --abort-on-container-exit",
|
|
34
|
+
"lint": "eslint .",
|
|
35
|
+
"sbom": "cyclonedx-npm --output-format json --output-file sbom.json",
|
|
36
|
+
"sbom:prod": "cyclonedx-npm --omit dev --output-format json --output-file sbom-prod.json",
|
|
37
|
+
"prepublishOnly": "npm run test && npm run lint"
|
|
38
|
+
},
|
|
39
|
+
"repository": {
|
|
40
|
+
"type": "git",
|
|
41
|
+
"url": "git+https://github.com/dependably/npm-check.git"
|
|
42
|
+
},
|
|
43
|
+
"homepage": "https://github.com/dependably/npm-check#readme",
|
|
44
|
+
"bugs": {
|
|
45
|
+
"url": "https://github.com/dependably/npm-check/issues"
|
|
46
|
+
},
|
|
47
|
+
"keywords": [
|
|
48
|
+
"npm",
|
|
49
|
+
"package-lock",
|
|
50
|
+
"npm-package-lock",
|
|
51
|
+
"validator",
|
|
52
|
+
"migrator",
|
|
53
|
+
"updater",
|
|
54
|
+
"lockfile",
|
|
55
|
+
"package-lock.json",
|
|
56
|
+
"integrity",
|
|
57
|
+
"hash",
|
|
58
|
+
"sha512",
|
|
59
|
+
"node-modules"
|
|
60
|
+
],
|
|
61
|
+
"author": {
|
|
62
|
+
"name": "MHiland",
|
|
63
|
+
"email": "michael@dependably.ca",
|
|
64
|
+
"url": "https://github.com/dependably"
|
|
65
|
+
},
|
|
66
|
+
"license": "Apache-2.0",
|
|
67
|
+
"engines": {
|
|
68
|
+
"node": ">=22.0.0",
|
|
69
|
+
"npm": ">=10.0.0"
|
|
70
|
+
},
|
|
71
|
+
"dependencies": {
|
|
72
|
+
"yaml": "2.9.0"
|
|
73
|
+
},
|
|
74
|
+
"devDependencies": {
|
|
75
|
+
"@babel/core": "7.29.7",
|
|
76
|
+
"@babel/preset-env": "7.28.6",
|
|
77
|
+
"@cyclonedx/cyclonedx-npm": "5.0.0",
|
|
78
|
+
"@eslint/js": "10.0.1",
|
|
79
|
+
"babel-jest": "30.2.0",
|
|
80
|
+
"eslint": "10.5.0",
|
|
81
|
+
"globals": "17.6.0",
|
|
82
|
+
"jest": "29.7.0",
|
|
83
|
+
"sonarqube-scanner": "3.5.0"
|
|
84
|
+
},
|
|
85
|
+
"allowScripts": {
|
|
86
|
+
"fsevents": true,
|
|
87
|
+
"libxmljs2": true
|
|
88
|
+
},
|
|
89
|
+
"overrides": {
|
|
90
|
+
"@babel/plugin-transform-modules-systemjs": "7.29.7",
|
|
91
|
+
"@ungap/structured-clone": "1.3.1",
|
|
92
|
+
"js-yaml": "4.2.0",
|
|
93
|
+
"picomatch": "4.0.4",
|
|
94
|
+
"glob": {
|
|
95
|
+
"minimatch": "3.1.5",
|
|
96
|
+
"brace-expansion": "1.1.15"
|
|
97
|
+
},
|
|
98
|
+
"test-exclude": {
|
|
99
|
+
"minimatch": "3.1.5",
|
|
100
|
+
"brace-expansion": "1.1.15"
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
// src/audit-config.js
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
|
|
5
|
+
export class AuditConfigError extends Error {
|
|
6
|
+
constructor(message, code, context = {}) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.name = 'AuditConfigError';
|
|
9
|
+
this.code = code;
|
|
10
|
+
this.context = context;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const CONFIG_FILENAMES = ['.npm-checkrc.json', 'npm-check.config.json'];
|
|
15
|
+
|
|
16
|
+
// Shared, cross-tool config file (JSON, no extension) discovered by walking up
|
|
17
|
+
// from the working directory. Its `common`/`npm` `allowedRegistryHosts` extend
|
|
18
|
+
// the secure-resolved trusted-host allowlist additively (public npm stays trusted).
|
|
19
|
+
export const SHARED_CONFIG_FILENAME = '.dependably-check';
|
|
20
|
+
|
|
21
|
+
export const SEVERITIES = ['error', 'warn', 'off'];
|
|
22
|
+
|
|
23
|
+
export const DEFAULT_CONFIG = {
|
|
24
|
+
maxWarnings: -1,
|
|
25
|
+
rules: {
|
|
26
|
+
'lockfile-version': ['error', { minVersion: 3 }],
|
|
27
|
+
'valid-structure': 'error',
|
|
28
|
+
'valid-package-json': 'error',
|
|
29
|
+
'integrity-hygiene': ['error', { allowSha1: false }],
|
|
30
|
+
'secure-resolved': ['error', {
|
|
31
|
+
allowedHosts: ['registry.npmjs.org'],
|
|
32
|
+
allowHttp: false,
|
|
33
|
+
allowGit: true,
|
|
34
|
+
allowFile: true
|
|
35
|
+
}],
|
|
36
|
+
'install-scripts': ['warn', { allow: [] }],
|
|
37
|
+
'no-git-deps': 'warn',
|
|
38
|
+
'no-remote-deps': ['warn', { allowedHosts: ['registry.npmjs.org', 'npm.pkg.github.com'] }],
|
|
39
|
+
'pinned-versions': ['warn', {
|
|
40
|
+
sections: ['dependencies', 'devDependencies', 'optionalDependencies'],
|
|
41
|
+
ignore: []
|
|
42
|
+
}],
|
|
43
|
+
'lockfile-sync': 'error',
|
|
44
|
+
'no-orphan-packages': 'warn',
|
|
45
|
+
'unused-dependencies': ['warn', { includeDev: false, ignore: [] }],
|
|
46
|
+
'no-fund': 'warn',
|
|
47
|
+
'valid-npmrc': ['warn', {}],
|
|
48
|
+
// pnpm-only rules (no-op on npm lockfiles via flavor gating in runAudit).
|
|
49
|
+
'valid-pnpm-workspace': 'error',
|
|
50
|
+
'valid-pnpm-field': 'error'
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export const KNOWN_RULES = Object.keys(DEFAULT_CONFIG.rules);
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Normalize a config rule entry to { severity, options }.
|
|
58
|
+
* Accepts 'error' | 'warn' | 'off' | [severity, options].
|
|
59
|
+
*/
|
|
60
|
+
export function normalizeRuleEntry(entry) {
|
|
61
|
+
let severity;
|
|
62
|
+
let options = {};
|
|
63
|
+
|
|
64
|
+
if (typeof entry === 'string') {
|
|
65
|
+
severity = entry;
|
|
66
|
+
} else if (Array.isArray(entry) && entry.length >= 1) {
|
|
67
|
+
severity = entry[0];
|
|
68
|
+
if (entry.length > 1) {
|
|
69
|
+
if (typeof entry[1] !== 'object' || entry[1] === null) {
|
|
70
|
+
throw new AuditConfigError(
|
|
71
|
+
`Rule options must be an object, got: ${JSON.stringify(entry[1])}`,
|
|
72
|
+
'INVALID_RULE_OPTIONS'
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
options = structuredClone(entry[1]);
|
|
76
|
+
}
|
|
77
|
+
} else {
|
|
78
|
+
throw new AuditConfigError(
|
|
79
|
+
`Invalid rule entry: ${JSON.stringify(entry)}`,
|
|
80
|
+
'INVALID_RULE_ENTRY'
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (!SEVERITIES.includes(severity)) {
|
|
85
|
+
throw new AuditConfigError(
|
|
86
|
+
`Invalid severity "${severity}" (expected: ${SEVERITIES.join(', ')})`,
|
|
87
|
+
'INVALID_SEVERITY'
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return { severity, options };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Resolve the effective audit config: built-in defaults overlaid by a
|
|
96
|
+
* discovered or explicitly-given JSON config file.
|
|
97
|
+
* Rule options merge over the defaults for that rule; severity replaces.
|
|
98
|
+
*
|
|
99
|
+
* @param {string} cwd - Directory to search for config files
|
|
100
|
+
* @param {string|null} explicitPath - Path passed via --config (wins over discovery)
|
|
101
|
+
* @returns {{maxWarnings, rules: {[id]: {severity, options}}, configPath}}
|
|
102
|
+
*/
|
|
103
|
+
/**
|
|
104
|
+
* Walk up from `cwd` to the filesystem root looking for the shared
|
|
105
|
+
* `.dependably-check` config file. Stops at the first hit, at a directory
|
|
106
|
+
* containing a `.git` entry (the repo root), or at the filesystem root.
|
|
107
|
+
*
|
|
108
|
+
* @param {string} cwd - Directory to start the search from
|
|
109
|
+
* @returns {string|null} Absolute path to the shared config, or null when absent
|
|
110
|
+
*/
|
|
111
|
+
export function findSharedConfig(cwd = process.cwd()) {
|
|
112
|
+
let dir = path.resolve(cwd);
|
|
113
|
+
for (;;) {
|
|
114
|
+
const candidate = path.join(dir, SHARED_CONFIG_FILENAME);
|
|
115
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
116
|
+
|
|
117
|
+
// Stop at the repo root: a directory containing `.git`.
|
|
118
|
+
if (fs.existsSync(path.join(dir, '.git'))) return null;
|
|
119
|
+
|
|
120
|
+
const parent = path.dirname(dir);
|
|
121
|
+
if (parent === dir) return null; // reached the filesystem root
|
|
122
|
+
dir = parent;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Union of `common.allowedRegistryHosts` and `npm.allowedRegistryHosts` from a
|
|
127
|
+
// parsed shared config (deduped, common first). Other sections are ignored.
|
|
128
|
+
function collectSharedHosts(parsed) {
|
|
129
|
+
const collect = (section) => {
|
|
130
|
+
const hosts = section && section.allowedRegistryHosts;
|
|
131
|
+
return Array.isArray(hosts) ? hosts.filter((h) => typeof h === 'string') : [];
|
|
132
|
+
};
|
|
133
|
+
return [...new Set([...collect(parsed.common), ...collect(parsed.npm)])];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Pull npm-check's audit settings (`rules`, `maxWarnings`) out of a shared
|
|
137
|
+
// `.dependably-check` object: the `common` section is the base, the `npm`
|
|
138
|
+
// section overrides it. Mirrors how the other suite tools read their section.
|
|
139
|
+
function extractSharedAuditSettings(parsed) {
|
|
140
|
+
const pick = (section) => {
|
|
141
|
+
const out = {};
|
|
142
|
+
if (section && typeof section === 'object') {
|
|
143
|
+
if (section.rules !== undefined) out.rules = section.rules;
|
|
144
|
+
if (section.maxWarnings !== undefined) out.maxWarnings = section.maxWarnings;
|
|
145
|
+
}
|
|
146
|
+
return out;
|
|
147
|
+
};
|
|
148
|
+
return { ...pick(parsed && parsed.common), ...pick(parsed && parsed.npm) };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// True when a parsed config is the shared `.dependably-check` shape (sectioned
|
|
152
|
+
// by tool) rather than the legacy flat tool-config shape (top-level rules/maxWarnings).
|
|
153
|
+
function isSharedShape(configPath, parsed) {
|
|
154
|
+
if (path.basename(configPath) === SHARED_CONFIG_FILENAME) return true;
|
|
155
|
+
if (!parsed || typeof parsed !== 'object') return false;
|
|
156
|
+
const hasToolKeys = 'rules' in parsed || 'maxWarnings' in parsed;
|
|
157
|
+
const hasSharedSections = 'common' in parsed || 'npm' in parsed;
|
|
158
|
+
return !hasToolKeys && hasSharedSections;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Read + JSON-parse a tool-config file, raising CONFIG_READ / CONFIG_PARSE.
|
|
162
|
+
function readJsonConfig(configPath) {
|
|
163
|
+
let raw;
|
|
164
|
+
try {
|
|
165
|
+
raw = fs.readFileSync(configPath, 'utf8');
|
|
166
|
+
} catch (e) {
|
|
167
|
+
throw new AuditConfigError(`Cannot read config file: ${e.message}`, 'CONFIG_READ', { configPath });
|
|
168
|
+
}
|
|
169
|
+
try {
|
|
170
|
+
return JSON.parse(raw);
|
|
171
|
+
} catch (e) {
|
|
172
|
+
throw new AuditConfigError(`Invalid JSON in ${configPath}: ${e.message}`, 'CONFIG_PARSE', { configPath });
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Read and parse the shared `.dependably-check` file, returning its registry-host
|
|
178
|
+
* allowlist and npm-check audit settings (`rules`/`maxWarnings`). Other tool
|
|
179
|
+
* sections and unknown keys are ignored.
|
|
180
|
+
*
|
|
181
|
+
* @param {string} cwd - Directory to start discovery from
|
|
182
|
+
* @returns {{ allowedRegistryHosts: string[], sharedPath: string|null, auditSettings: object }}
|
|
183
|
+
*/
|
|
184
|
+
export function loadSharedConfig(cwd = process.cwd()) {
|
|
185
|
+
const sharedPath = findSharedConfig(cwd);
|
|
186
|
+
if (!sharedPath) return { allowedRegistryHosts: [], sharedPath: null, auditSettings: {} };
|
|
187
|
+
|
|
188
|
+
let raw;
|
|
189
|
+
try {
|
|
190
|
+
raw = fs.readFileSync(sharedPath, 'utf8');
|
|
191
|
+
} catch (e) {
|
|
192
|
+
throw new AuditConfigError(`Cannot read shared config file: ${e.message}`, 'SHARED_CONFIG_READ', { sharedPath });
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
let parsed;
|
|
196
|
+
try {
|
|
197
|
+
parsed = JSON.parse(raw);
|
|
198
|
+
} catch (e) {
|
|
199
|
+
throw new AuditConfigError(`Invalid JSON in ${sharedPath}: ${e.message}`, 'SHARED_CONFIG_PARSE', { sharedPath });
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
allowedRegistryHosts: collectSharedHosts(parsed),
|
|
204
|
+
sharedPath,
|
|
205
|
+
auditSettings: extractSharedAuditSettings(parsed)
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export function loadAuditConfig(cwd = process.cwd(), explicitPath = null) {
|
|
210
|
+
// The shared `.dependably-check` (discovered by walking up to the repo root)
|
|
211
|
+
// is the PRIMARY config source — its `common`/`npm` sections supply the base
|
|
212
|
+
// audit settings and the registry-host allowlist. A tool-specific
|
|
213
|
+
// `.npm-checkrc.json` (or an explicit `--config`) overrides it.
|
|
214
|
+
const shared = loadSharedConfig(cwd);
|
|
215
|
+
|
|
216
|
+
let toolConfig = {};
|
|
217
|
+
let configPath = null;
|
|
218
|
+
let explicitSharedHosts = [];
|
|
219
|
+
|
|
220
|
+
if (explicitPath) {
|
|
221
|
+
configPath = path.resolve(explicitPath);
|
|
222
|
+
if (!fs.existsSync(configPath)) {
|
|
223
|
+
throw new AuditConfigError(`Config file not found: ${configPath}`, 'CONFIG_NOT_FOUND');
|
|
224
|
+
}
|
|
225
|
+
const parsed = readJsonConfig(configPath);
|
|
226
|
+
if (isSharedShape(configPath, parsed)) {
|
|
227
|
+
// `--config` points at a `.dependably-check`: read npm-check's settings
|
|
228
|
+
// from its common/npm sections and take its registry-host allowlist too.
|
|
229
|
+
toolConfig = extractSharedAuditSettings(parsed);
|
|
230
|
+
explicitSharedHosts = collectSharedHosts(parsed);
|
|
231
|
+
} else {
|
|
232
|
+
// Legacy flat tool-config (.npm-checkrc.json shape) given explicitly.
|
|
233
|
+
toolConfig = parsed;
|
|
234
|
+
}
|
|
235
|
+
} else {
|
|
236
|
+
// Discover a tool-specific config in the working directory (fallback for
|
|
237
|
+
// back-compat; the shared `.dependably-check` above is the primary source).
|
|
238
|
+
for (const name of CONFIG_FILENAMES) {
|
|
239
|
+
const candidate = path.join(cwd, name);
|
|
240
|
+
if (fs.existsSync(candidate)) {
|
|
241
|
+
configPath = candidate;
|
|
242
|
+
toolConfig = readJsonConfig(candidate);
|
|
243
|
+
break;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Shared audit settings are the base; the tool-specific config overrides them.
|
|
249
|
+
const userConfig = { ...shared.auditSettings, ...toolConfig };
|
|
250
|
+
const config = mergeConfig(userConfig, configPath || shared.sharedPath);
|
|
251
|
+
|
|
252
|
+
// Layer the shared `.dependably-check` hosts ADDITIVELY onto whatever
|
|
253
|
+
// secure-resolved.allowedHosts resolved to (built-in default or a
|
|
254
|
+
// tool-config replacement) — public npm always stays trusted.
|
|
255
|
+
const hosts = [...new Set([...shared.allowedRegistryHosts, ...explicitSharedHosts])];
|
|
256
|
+
if (hosts.length > 0) {
|
|
257
|
+
extendAllowedHosts(config, hosts);
|
|
258
|
+
}
|
|
259
|
+
config.sharedConfigPath = shared.sharedPath;
|
|
260
|
+
|
|
261
|
+
return config;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Add the given hosts to every host-based rule's `allowedHosts`, deduplicated,
|
|
266
|
+
* without replacing the existing entries. No-op for rules that are absent.
|
|
267
|
+
*
|
|
268
|
+
* BOTH `secure-resolved` (which registries are trusted for HTTPS resolution) and
|
|
269
|
+
* `no-remote-deps` (which registry hosts count as a registry rather than a
|
|
270
|
+
* remote/git dep) consult `allowedHosts`. The shared `.dependably-check`
|
|
271
|
+
* allowlist must reach both — otherwise a private-registry project silences one
|
|
272
|
+
* rule but still trips the other, breaking the documented `--fail-on` CI gate.
|
|
273
|
+
*
|
|
274
|
+
* @param {object} config - A merged audit config (from mergeConfig)
|
|
275
|
+
* @param {string[]} hosts - Bare hostnames to add to the allowlist
|
|
276
|
+
*/
|
|
277
|
+
export function extendAllowedHosts(config, hosts) {
|
|
278
|
+
for (const ruleId of ['secure-resolved', 'no-remote-deps']) {
|
|
279
|
+
const rule = config.rules && config.rules[ruleId];
|
|
280
|
+
if (!rule) continue;
|
|
281
|
+
const opts = rule.options || {};
|
|
282
|
+
const existing = Array.isArray(opts.allowedHosts) ? opts.allowedHosts : [];
|
|
283
|
+
rule.options = { ...opts, allowedHosts: [...new Set([...existing, ...hosts])] };
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Merge a user config object over the defaults, validating rule ids and severities.
|
|
289
|
+
* @param {object} userConfig - Parsed user config ({ maxWarnings?, rules? })
|
|
290
|
+
* @param {string|null} configPath - Where it came from (for reporting)
|
|
291
|
+
*/
|
|
292
|
+
export function mergeConfig(userConfig = {}, configPath = null) {
|
|
293
|
+
if (userConfig.rules) {
|
|
294
|
+
for (const ruleId of Object.keys(userConfig.rules)) {
|
|
295
|
+
if (!KNOWN_RULES.includes(ruleId)) {
|
|
296
|
+
throw new AuditConfigError(
|
|
297
|
+
`Unknown rule "${ruleId}" (known rules: ${KNOWN_RULES.join(', ')})`,
|
|
298
|
+
'UNKNOWN_RULE',
|
|
299
|
+
{ ruleId, configPath }
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const rules = {};
|
|
306
|
+
for (const ruleId of KNOWN_RULES) {
|
|
307
|
+
const defaults = normalizeRuleEntry(DEFAULT_CONFIG.rules[ruleId]);
|
|
308
|
+
if (userConfig.rules && userConfig.rules[ruleId] !== undefined) {
|
|
309
|
+
const user = normalizeRuleEntry(userConfig.rules[ruleId]);
|
|
310
|
+
rules[ruleId] = {
|
|
311
|
+
severity: user.severity,
|
|
312
|
+
options: { ...defaults.options, ...user.options }
|
|
313
|
+
};
|
|
314
|
+
} else {
|
|
315
|
+
rules[ruleId] = defaults;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
let maxWarnings = DEFAULT_CONFIG.maxWarnings;
|
|
320
|
+
if (userConfig.maxWarnings !== undefined) {
|
|
321
|
+
if (typeof userConfig.maxWarnings !== 'number' || !Number.isInteger(userConfig.maxWarnings)) {
|
|
322
|
+
throw new AuditConfigError(
|
|
323
|
+
`maxWarnings must be an integer, got: ${JSON.stringify(userConfig.maxWarnings)}`,
|
|
324
|
+
'INVALID_MAX_WARNINGS'
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
maxWarnings = userConfig.maxWarnings;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
return { maxWarnings, rules, configPath };
|
|
331
|
+
}
|