@dependably/npm-check 1.7.0 → 1.7.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 +1 -5
- package/bin/cli.js +52 -16
- package/package.json +1 -1
- package/src/audit-config.js +215 -62
- package/src/audit.js +73 -8
- package/src/exceptions.js +281 -0
- package/src/progress-reporter.js +33 -1
- package/src/report.js +261 -48
package/README.md
CHANGED
|
@@ -60,11 +60,7 @@ for every command, flag, and exit code.
|
|
|
60
60
|
|
|
61
61
|
- **[CLI reference](https://github.com/dependably/npm-check/blob/main/docs/CLI.md)** — all commands, flags, exit codes, JSON output
|
|
62
62
|
- **[API guide](https://github.com/dependably/npm-check/blob/main/docs/API.md)** — using it as a library
|
|
63
|
-
- [
|
|
64
|
-
[Testing](https://github.com/dependably/npm-check/blob/main/docs/TESTING.md)
|
|
65
|
-
- [Changelog](https://github.com/dependably/npm-check/blob/main/CHANGELOG.md) ·
|
|
66
|
-
[Contributing](https://github.com/dependably/npm-check/blob/main/CONTRIBUTING.md) ·
|
|
67
|
-
[Security](https://github.com/dependably/npm-check/blob/main/SECURITY.md)
|
|
63
|
+
- [Changelog](https://github.com/dependably/npm-check/blob/main/CHANGELOG.md)
|
|
68
64
|
|
|
69
65
|
## License
|
|
70
66
|
|
package/bin/cli.js
CHANGED
|
@@ -11,7 +11,7 @@ import { migrateToVersion } from '../src/migrator.js';
|
|
|
11
11
|
import { upgradeIntegrityHashes, deduplicatePackages } from '../src/updater.js';
|
|
12
12
|
import { fixPackageLock } from '../src/fixer.js';
|
|
13
13
|
import { createBackup, listBackups, restoreFromLatestBackup, cleanOldBackups, BackupError } from '../src/backup.js';
|
|
14
|
-
import {
|
|
14
|
+
import { formatCliProgressUpdate } from '../src/progress-reporter.js';
|
|
15
15
|
import { checkIntegrity, checkLicenses } from '../src/checker.js';
|
|
16
16
|
import { checkVulnerabilities, vulnEnvelope } from '../src/vuln.js';
|
|
17
17
|
import { checkDeprecations, deprecationEnvelope } from '../src/deprecation.js';
|
|
@@ -82,11 +82,14 @@ Report Options:
|
|
|
82
82
|
--no-vuln Skip the known-vulnerability scan
|
|
83
83
|
--no-deprecated Skip the deprecation scan
|
|
84
84
|
--no-license Skip the license check
|
|
85
|
-
--config <file> Suite config (.dependably-check
|
|
86
|
-
|
|
85
|
+
--config <file> Suite config (.dependably; .dependably-check is a
|
|
86
|
+
deprecated alias), discovered by walking up to the
|
|
87
|
+
repo root; .npm-checkrc.json is a fallback
|
|
87
88
|
--format human|json Output format (default: human; json emits the shared finding schema)
|
|
88
89
|
--allow-unresolved Don't fail when a registry-backed scan can't complete
|
|
89
90
|
(registry down / endpoint unsupported). Default: FAIL CLOSED
|
|
91
|
+
--verbose List every "Remote-URL deps" package individually instead of
|
|
92
|
+
the default grouped-by-host cross-reference against "Resolved URLs"
|
|
90
93
|
--concurrency / --timeout / --registry / --licenses-csv (as in Check Options)
|
|
91
94
|
|
|
92
95
|
Check Options:
|
|
@@ -145,11 +148,13 @@ Unused Options:
|
|
|
145
148
|
--format human|json Output format (default: human; json is machine-readable)
|
|
146
149
|
|
|
147
150
|
Audit Options:
|
|
148
|
-
--config <file> Suite config (.dependably-check
|
|
149
|
-
|
|
151
|
+
--config <file> Suite config (.dependably; .dependably-check is a
|
|
152
|
+
deprecated alias), discovered by walking up to the
|
|
153
|
+
repo root; .npm-checkrc.json is a fallback
|
|
150
154
|
--rule <id>:<severity> Override a rule severity (error|warn|off); repeatable
|
|
151
155
|
--fail-on count=<N> Fail when the warning count exceeds N (count=0 fails on
|
|
152
156
|
any warning; --max-warnings / --strict are deprecated aliases)
|
|
157
|
+
--show-suppressed List findings suppressed by .dependably exceptions
|
|
153
158
|
--format stylish|json Report format (default: stylish)
|
|
154
159
|
|
|
155
160
|
General Options:
|
|
@@ -266,15 +271,16 @@ function handleError(error, context = '') {
|
|
|
266
271
|
process.exit(2);
|
|
267
272
|
}
|
|
268
273
|
|
|
269
|
-
// Build a progress callback that
|
|
270
|
-
//
|
|
274
|
+
// Build a progress callback that writes to stderr (stdout stays report-only).
|
|
275
|
+
// On a real TTY it redraws an animated `\r` bar in place; when stdout is NOT a
|
|
276
|
+
// TTY (piped, redirected, `tee`, CI logs) it degrades to periodic one-line
|
|
277
|
+
// milestones instead — see formatCliProgressUpdate() for why.
|
|
271
278
|
function makeProgressReporter() {
|
|
272
|
-
|
|
279
|
+
const isTTY = Boolean(process.stdout.isTTY);
|
|
280
|
+
const state = { lastPercentage: -1, lastMilestone: -1 };
|
|
273
281
|
return (progress) => {
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
lastProgress = progress;
|
|
277
|
-
}
|
|
282
|
+
const line = formatCliProgressUpdate(progress, state, isTTY);
|
|
283
|
+
if (line) process.stderr.write(line);
|
|
278
284
|
};
|
|
279
285
|
}
|
|
280
286
|
|
|
@@ -325,7 +331,8 @@ const VALUED_OPTIONS = new Set([
|
|
|
325
331
|
const BOOLEAN_OPTIONS = new Set([
|
|
326
332
|
'--offline', '--allow-unresolved',
|
|
327
333
|
'--no-integrity', '--no-vuln', '--no-deprecated', '--no-license',
|
|
328
|
-
'--include-dev', '--include-peer', '--write', '--local-fallback',
|
|
334
|
+
'--include-dev', '--include-peer', '--write', '--local-fallback', '--verbose',
|
|
335
|
+
'--show-suppressed',
|
|
329
336
|
'--version', '--help', '-h',
|
|
330
337
|
// deprecated boolean aliases (still parsed)
|
|
331
338
|
'--strict', '--fail-on-deprecated'
|
|
@@ -496,8 +503,11 @@ function registryOption(defaultRegistry) {
|
|
|
496
503
|
return defaultRegistry ? { defaultRegistry } : {};
|
|
497
504
|
}
|
|
498
505
|
|
|
499
|
-
// Clear the in-progress progress bar line.
|
|
506
|
+
// Clear the in-progress progress bar line. Only meaningful on a real TTY —
|
|
507
|
+
// the non-TTY reporter never draws an in-place line to begin with, so this
|
|
508
|
+
// would otherwise just emit a stray blank-ish line into piped/CI output.
|
|
500
509
|
function clearProgressLine() {
|
|
510
|
+
if (!process.stdout.isTTY) return;
|
|
501
511
|
process.stderr.write('\r' + ' '.repeat(80) + '\r');
|
|
502
512
|
}
|
|
503
513
|
|
|
@@ -610,6 +620,9 @@ function parseReportOptions() {
|
|
|
610
620
|
strict,
|
|
611
621
|
maxWarnings,
|
|
612
622
|
format: parseFormatFlag(['human', 'json'], 'human'),
|
|
623
|
+
// List every duplicated "Remote-URL deps" package individually instead of
|
|
624
|
+
// the default grouped-by-host cross-reference against "Resolved URLs".
|
|
625
|
+
verbose: argv.includes('--verbose'),
|
|
613
626
|
// Network/integrity + license toggles.
|
|
614
627
|
integrity: !argv.includes('--offline') && !argv.includes('--no-integrity'),
|
|
615
628
|
license: !argv.includes('--no-license'),
|
|
@@ -650,7 +663,7 @@ async function runReportCommand() {
|
|
|
650
663
|
auditConfig: opts.config, integrity: opts.integrity, license: opts.license, vuln: opts.vuln,
|
|
651
664
|
deprecated: opts.deprecated, failOnDeprecated: opts.failOnDeprecated, minSeverity: opts.minSeverity,
|
|
652
665
|
strict: opts.strict, maxWarnings: opts.maxWarnings, concurrency: opts.concurrency,
|
|
653
|
-
timeoutMs: opts.timeoutMs, failOnUnresolved: opts.failOnUnresolved, onProgress,
|
|
666
|
+
timeoutMs: opts.timeoutMs, failOnUnresolved: opts.failOnUnresolved, verbose: opts.verbose, onProgress,
|
|
654
667
|
...registryOption(opts.defaultRegistry),
|
|
655
668
|
...(opts.licensesCsv ? { licensesCsv: opts.licensesCsv } : {})
|
|
656
669
|
}
|
|
@@ -980,6 +993,24 @@ function runUnusedCommand() {
|
|
|
980
993
|
}
|
|
981
994
|
}
|
|
982
995
|
|
|
996
|
+
// Print .dependably config notices (deprecated filename/section, unknown keys)
|
|
997
|
+
// to stderr. Never affects exit codes or the JSON payload on stdout.
|
|
998
|
+
function emitConfigWarnings(warnings, format) {
|
|
999
|
+
if (format === 'json' || !Array.isArray(warnings)) return;
|
|
1000
|
+
for (const w of warnings) console.error(`.dependably: ${w.message}`);
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
// Print unused / expired exception notices to stderr (spec §6.4/§6.5).
|
|
1004
|
+
function emitExceptionWarnings(meta, format) {
|
|
1005
|
+
if (format === 'json' || !meta) return;
|
|
1006
|
+
for (const ex of meta.expired || []) {
|
|
1007
|
+
console.error(`.dependably: exception expired ${ex.expires} for rule "${ex.rule}" — ${ex.reason}`);
|
|
1008
|
+
}
|
|
1009
|
+
for (const ex of meta.unused || []) {
|
|
1010
|
+
console.error(`.dependably: unused exception for rule "${ex.rule}" — ${ex.reason}`);
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
|
|
983
1014
|
function runAuditCommand() {
|
|
984
1015
|
const filePath = getFilePath(positionals()[0]);
|
|
985
1016
|
// The audit command gates on warning/finding count, not severity level.
|
|
@@ -998,13 +1029,18 @@ function runAuditCommand() {
|
|
|
998
1029
|
applyRuleOverrides(config);
|
|
999
1030
|
applyMaxWarnings(config);
|
|
1000
1031
|
const format = parseFormatFlag(['stylish', 'json'], 'stylish');
|
|
1032
|
+
const showSuppressed = argv.includes('--show-suppressed');
|
|
1033
|
+
|
|
1034
|
+
// Surface .dependably deprecation / unknown-key notices (never gating).
|
|
1035
|
+
emitConfigWarnings(config.warnings, format);
|
|
1001
1036
|
|
|
1002
1037
|
const lockfile = parseLockfile(filePath);
|
|
1003
1038
|
// package.json is optional; the pinned-versions rule degrades gracefully
|
|
1004
1039
|
const packageJson = loadSiblingPackageJson(filePath);
|
|
1005
1040
|
|
|
1006
1041
|
report = runAudit({ lockfile, packageJson, filePath: path.relative(process.cwd(), filePath) || filePath }, config);
|
|
1007
|
-
|
|
1042
|
+
emitExceptionWarnings(report.exceptionsMeta, format);
|
|
1043
|
+
console.log('\n' + formatAuditReport(report, { format, showSuppressed }));
|
|
1008
1044
|
} catch (error) {
|
|
1009
1045
|
console.error(`\nAudit error: ${error.message}`);
|
|
1010
1046
|
process.exit(2);
|
package/package.json
CHANGED
package/src/audit-config.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// src/audit-config.js
|
|
2
2
|
import fs from 'fs';
|
|
3
3
|
import path from 'path';
|
|
4
|
+
import { parseExceptions } from './exceptions.js';
|
|
4
5
|
|
|
5
6
|
export class AuditConfigError extends Error {
|
|
6
7
|
constructor(message, code, context = {}) {
|
|
@@ -14,9 +15,30 @@ export class AuditConfigError extends Error {
|
|
|
14
15
|
export const CONFIG_FILENAMES = ['.npm-checkrc.json', 'npm-check.config.json'];
|
|
15
16
|
|
|
16
17
|
// Shared, cross-tool config file (JSON, no extension) discovered by walking up
|
|
17
|
-
// from the working directory.
|
|
18
|
-
//
|
|
19
|
-
export const SHARED_CONFIG_FILENAME = '.dependably
|
|
18
|
+
// from the working directory. `.dependably` is canonical; `.dependably-check` is
|
|
19
|
+
// a deprecated alias kept for the migration window (docs/dependably-config-spec.md §7).
|
|
20
|
+
export const SHARED_CONFIG_FILENAME = '.dependably';
|
|
21
|
+
export const DEPRECATED_SHARED_CONFIG_FILENAME = '.dependably-check';
|
|
22
|
+
// Checked in this order at each directory level (canonical wins).
|
|
23
|
+
export const SHARED_CONFIG_FILENAMES = [SHARED_CONFIG_FILENAME, DEPRECATED_SHARED_CONFIG_FILENAME];
|
|
24
|
+
|
|
25
|
+
// Canonical section key for npm-check, plus the deprecated ecosystem alias.
|
|
26
|
+
export const SECTION_KEY = 'npm-check';
|
|
27
|
+
export const DEPRECATED_SECTION_KEY = 'npm';
|
|
28
|
+
|
|
29
|
+
// Highest .dependably format version this build understands.
|
|
30
|
+
export const SUPPORTED_CONFIG_VERSION = 1;
|
|
31
|
+
|
|
32
|
+
// Exception selectors an npm-check finding can carry (spec §6.7). `path`/`symbol`
|
|
33
|
+
// are code-location selectors used by the C# tools; they are errors in npm-check's
|
|
34
|
+
// own section but tolerated (ignored) in `common`.
|
|
35
|
+
export const APPLICABLE_SELECTORS = ['package', 'id'];
|
|
36
|
+
|
|
37
|
+
// Keys npm-check recognizes inside `common` / its own section. Unknown keys warn.
|
|
38
|
+
const KNOWN_SECTION_KEYS = new Set([
|
|
39
|
+
'rules', 'exceptions', 'exclude', 'failOn',
|
|
40
|
+
'allowedRegistryHosts', 'allowedLocalFeeds', 'maxWarnings'
|
|
41
|
+
]);
|
|
20
42
|
|
|
21
43
|
export const SEVERITIES = ['error', 'warn', 'off'];
|
|
22
44
|
|
|
@@ -92,18 +114,10 @@ export function normalizeRuleEntry(entry) {
|
|
|
92
114
|
}
|
|
93
115
|
|
|
94
116
|
/**
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
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.
|
|
117
|
+
* Walk up from `cwd` to the filesystem root looking for a shared config file.
|
|
118
|
+
* At each level `.dependably` is preferred over the deprecated `.dependably-check`.
|
|
119
|
+
* Stops at the first hit, at a directory containing a `.git` entry (the repo
|
|
120
|
+
* root), or at the filesystem root.
|
|
107
121
|
*
|
|
108
122
|
* @param {string} cwd - Directory to start the search from
|
|
109
123
|
* @returns {string|null} Absolute path to the shared config, or null when absent
|
|
@@ -111,8 +125,10 @@ export function normalizeRuleEntry(entry) {
|
|
|
111
125
|
export function findSharedConfig(cwd = process.cwd()) {
|
|
112
126
|
let dir = path.resolve(cwd);
|
|
113
127
|
for (;;) {
|
|
114
|
-
const
|
|
115
|
-
|
|
128
|
+
for (const name of SHARED_CONFIG_FILENAMES) {
|
|
129
|
+
const candidate = path.join(dir, name);
|
|
130
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
131
|
+
}
|
|
116
132
|
|
|
117
133
|
// Stop at the repo root: a directory containing `.git`.
|
|
118
134
|
if (fs.existsSync(path.join(dir, '.git'))) return null;
|
|
@@ -123,38 +139,132 @@ export function findSharedConfig(cwd = process.cwd()) {
|
|
|
123
139
|
}
|
|
124
140
|
}
|
|
125
141
|
|
|
126
|
-
//
|
|
127
|
-
|
|
142
|
+
// Deprecation warnings for the selected shared-config file (§2.2/§2.3).
|
|
143
|
+
function filenameWarnings(sharedPath) {
|
|
144
|
+
if (!sharedPath) return [];
|
|
145
|
+
const dir = path.dirname(sharedPath);
|
|
146
|
+
const base = path.basename(sharedPath);
|
|
147
|
+
const warnings = [];
|
|
148
|
+
if (base === DEPRECATED_SHARED_CONFIG_FILENAME) {
|
|
149
|
+
if (fs.existsSync(path.join(dir, SHARED_CONFIG_FILENAME))) {
|
|
150
|
+
// Both present but findSharedConfig preferred canonical — should not reach
|
|
151
|
+
// here; keep the guard for direct callers.
|
|
152
|
+
warnings.push({ code: 'BOTH_FILES_PRESENT', message: `both ${SHARED_CONFIG_FILENAME} and ${DEPRECATED_SHARED_CONFIG_FILENAME} found in ${dir}; using ${SHARED_CONFIG_FILENAME}` });
|
|
153
|
+
} else {
|
|
154
|
+
warnings.push({ code: 'DEPRECATED_FILENAME', message: `${DEPRECATED_SHARED_CONFIG_FILENAME} is deprecated; rename it to ${SHARED_CONFIG_FILENAME}` });
|
|
155
|
+
}
|
|
156
|
+
} else if (fs.existsSync(path.join(dir, DEPRECATED_SHARED_CONFIG_FILENAME))) {
|
|
157
|
+
warnings.push({ code: 'BOTH_FILES_PRESENT', message: `both ${SHARED_CONFIG_FILENAME} and ${DEPRECATED_SHARED_CONFIG_FILENAME} found in ${dir}; using ${SHARED_CONFIG_FILENAME} (${DEPRECATED_SHARED_CONFIG_FILENAME} is ignored — delete it)` });
|
|
158
|
+
}
|
|
159
|
+
return warnings;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Bare hostnames from a section's allowedRegistryHosts (lowercased, filtered).
|
|
163
|
+
function sectionHosts(section) {
|
|
164
|
+
const hosts = section && section.allowedRegistryHosts;
|
|
165
|
+
if (!Array.isArray(hosts)) return [];
|
|
166
|
+
return hosts.filter((h) => typeof h === 'string').map((h) => h.trim().toLowerCase()).filter(Boolean);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Union of `common` and the npm-check section's allowedRegistryHosts (deduped,
|
|
170
|
+
// case-insensitive, common first). The canonical section is preferred; the `npm`
|
|
171
|
+
// alias is read only when the canonical section is absent.
|
|
128
172
|
function collectSharedHosts(parsed) {
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
return Array.isArray(hosts) ? hosts.filter((h) => typeof h === 'string') : [];
|
|
132
|
-
};
|
|
133
|
-
return [...new Set([...collect(parsed.common), ...collect(parsed.npm)])];
|
|
173
|
+
const tool = parsed[SECTION_KEY] !== undefined ? parsed[SECTION_KEY] : parsed[DEPRECATED_SECTION_KEY];
|
|
174
|
+
return [...new Set([...sectionHosts(parsed && parsed.common), ...sectionHosts(tool)])];
|
|
134
175
|
}
|
|
135
176
|
|
|
136
|
-
//
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
177
|
+
// Union two arrays (ordinal dedupe), tolerating non-arrays.
|
|
178
|
+
function unionList(a, b) {
|
|
179
|
+
const out = [];
|
|
180
|
+
const seen = new Set();
|
|
181
|
+
for (const v of [...(Array.isArray(a) ? a : []), ...(Array.isArray(b) ? b : [])]) {
|
|
182
|
+
if (!seen.has(v)) { seen.add(v); out.push(v); }
|
|
183
|
+
}
|
|
184
|
+
return out;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Merge the `rules` maps of two sections per rule-id: the tool entry for a given
|
|
188
|
+
// id replaces common's wholesale (no cross-section option deep-merge, §B.3).
|
|
189
|
+
function mergeRuleMaps(commonRules, toolRules) {
|
|
190
|
+
if (!commonRules && !toolRules) return undefined;
|
|
191
|
+
return { ...(commonRules && typeof commonRules === 'object' ? commonRules : {}),
|
|
192
|
+
...(toolRules && typeof toolRules === 'object' ? toolRules : {}) };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Warn about keys npm-check does not recognize inside a read section (§8).
|
|
196
|
+
function unknownKeyWarnings(section, label, warnings) {
|
|
197
|
+
if (!section || typeof section !== 'object') return;
|
|
198
|
+
for (const key of Object.keys(section)) {
|
|
199
|
+
if (!KNOWN_SECTION_KEYS.has(key)) {
|
|
200
|
+
warnings.push({ code: 'UNKNOWN_KEY', message: `unknown key "${label}.${key}" in shared config — ignoring` });
|
|
145
201
|
}
|
|
146
|
-
|
|
147
|
-
};
|
|
148
|
-
return { ...pick(parsed && parsed.common), ...pick(parsed && parsed.npm) };
|
|
202
|
+
}
|
|
149
203
|
}
|
|
150
204
|
|
|
151
|
-
|
|
152
|
-
|
|
205
|
+
/**
|
|
206
|
+
* Resolve npm-check's settings from a parsed shared-config object: merge `common`
|
|
207
|
+
* under the npm-check section per the single merge rule (§5). Returns the audit
|
|
208
|
+
* settings plus parsed exceptions and any warnings.
|
|
209
|
+
*/
|
|
210
|
+
function resolveToolSection(parsed, warnings) {
|
|
211
|
+
const common = parsed && parsed.common;
|
|
212
|
+
const canonical = parsed && parsed[SECTION_KEY];
|
|
213
|
+
const alias = parsed && parsed[DEPRECATED_SECTION_KEY];
|
|
214
|
+
const tool = canonical !== undefined ? canonical : alias;
|
|
215
|
+
if (canonical === undefined && alias !== undefined) {
|
|
216
|
+
warnings.push({ code: 'DEPRECATED_ALIAS_SECTION', message: `section "${DEPRECATED_SECTION_KEY}" is deprecated; rename it to "${SECTION_KEY}"` });
|
|
217
|
+
} else if (canonical !== undefined && alias !== undefined) {
|
|
218
|
+
warnings.push({ code: 'DEPRECATED_ALIAS_SECTION', message: `both "${SECTION_KEY}" and "${DEPRECATED_SECTION_KEY}" sections present; using "${SECTION_KEY}"` });
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
unknownKeyWarnings(common, 'common', warnings);
|
|
222
|
+
unknownKeyWarnings(tool, SECTION_KEY, warnings);
|
|
223
|
+
|
|
224
|
+
const settings = {};
|
|
225
|
+
const rules = mergeRuleMaps(common && common.rules, tool && tool.rules);
|
|
226
|
+
if (rules) settings.rules = rules;
|
|
227
|
+
|
|
228
|
+
// Scalars: tool overrides common. failOn merges per key.
|
|
229
|
+
const failOn = pickFailOn(common && common.failOn, tool && tool.failOn);
|
|
230
|
+
if (failOn) settings.failOn = failOn;
|
|
231
|
+
|
|
232
|
+
const pickMax = (s) => (s && s.maxWarnings !== undefined ? s.maxWarnings : undefined);
|
|
233
|
+
const toolMax = pickMax(tool);
|
|
234
|
+
const commonMax = pickMax(common);
|
|
235
|
+
if (toolMax !== undefined) settings.maxWarnings = toolMax;
|
|
236
|
+
else if (commonMax !== undefined) settings.maxWarnings = commonMax;
|
|
237
|
+
|
|
238
|
+
settings.exclude = unionList(common && common.exclude, tool && tool.exclude);
|
|
239
|
+
|
|
240
|
+
// Exceptions: common (tolerant) + own section (strict selector/rule checks).
|
|
241
|
+
const commonEx = parseExceptions(common && common.exceptions, {
|
|
242
|
+
source: 'common', applicableSelectors: APPLICABLE_SELECTORS
|
|
243
|
+
});
|
|
244
|
+
const ownEx = parseExceptions(tool && tool.exceptions, {
|
|
245
|
+
source: 'own', applicableSelectors: APPLICABLE_SELECTORS, knownRules: KNOWN_RULES
|
|
246
|
+
});
|
|
247
|
+
settings.exceptions = [...commonEx, ...ownEx];
|
|
248
|
+
|
|
249
|
+
return settings;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Merge two failOn objects per key (tool wins). Returns undefined if neither set.
|
|
253
|
+
function pickFailOn(commonFailOn, toolFailOn) {
|
|
254
|
+
const c = commonFailOn && typeof commonFailOn === 'object' ? commonFailOn : {};
|
|
255
|
+
const t = toolFailOn && typeof toolFailOn === 'object' ? toolFailOn : {};
|
|
256
|
+
const merged = { ...c, ...t };
|
|
257
|
+
if (merged.severity === undefined && merged.count === undefined) return undefined;
|
|
258
|
+
return merged;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// True when a parsed config is the shared (sectioned) shape rather than the
|
|
262
|
+
// legacy flat tool-config shape (top-level rules/maxWarnings).
|
|
153
263
|
function isSharedShape(configPath, parsed) {
|
|
154
|
-
if (path.basename(configPath)
|
|
264
|
+
if (SHARED_CONFIG_FILENAMES.includes(path.basename(configPath))) return true;
|
|
155
265
|
if (!parsed || typeof parsed !== 'object') return false;
|
|
156
266
|
const hasToolKeys = 'rules' in parsed || 'maxWarnings' in parsed;
|
|
157
|
-
const hasSharedSections = 'common' in parsed ||
|
|
267
|
+
const hasSharedSections = 'common' in parsed || SECTION_KEY in parsed || DEPRECATED_SECTION_KEY in parsed;
|
|
158
268
|
return !hasToolKeys && hasSharedSections;
|
|
159
269
|
}
|
|
160
270
|
|
|
@@ -173,17 +283,35 @@ function readJsonConfig(configPath) {
|
|
|
173
283
|
}
|
|
174
284
|
}
|
|
175
285
|
|
|
286
|
+
// Validate the top-level shape + version of a shared-config object (§3, §8).
|
|
287
|
+
function validateSharedShape(parsed, sharedPath) {
|
|
288
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
289
|
+
throw new AuditConfigError(`Shared config must be a JSON object: ${sharedPath}`, 'CONFIG_SHAPE', { sharedPath });
|
|
290
|
+
}
|
|
291
|
+
if (parsed.version !== undefined) {
|
|
292
|
+
if (typeof parsed.version !== 'number' || !Number.isInteger(parsed.version) || parsed.version > SUPPORTED_CONFIG_VERSION) {
|
|
293
|
+
throw new AuditConfigError(
|
|
294
|
+
`Unsupported .dependably version ${JSON.stringify(parsed.version)} (this build supports up to ${SUPPORTED_CONFIG_VERSION})`,
|
|
295
|
+
'CONFIG_VERSION',
|
|
296
|
+
{ sharedPath }
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
176
302
|
/**
|
|
177
|
-
* Read and parse the shared
|
|
178
|
-
* allowlist
|
|
179
|
-
*
|
|
303
|
+
* Read and parse the shared config file, returning npm-check's registry-host
|
|
304
|
+
* allowlist, audit settings (`rules`/`maxWarnings`/`failOn`/`exclude`),
|
|
305
|
+
* parsed exceptions, and any deprecation/unknown-key warnings.
|
|
180
306
|
*
|
|
181
307
|
* @param {string} cwd - Directory to start discovery from
|
|
182
|
-
* @returns {{ allowedRegistryHosts
|
|
308
|
+
* @returns {{ allowedRegistryHosts, sharedPath, auditSettings, exceptions, exclude, failOn, warnings }}
|
|
183
309
|
*/
|
|
184
310
|
export function loadSharedConfig(cwd = process.cwd()) {
|
|
185
311
|
const sharedPath = findSharedConfig(cwd);
|
|
186
|
-
if (!sharedPath)
|
|
312
|
+
if (!sharedPath) {
|
|
313
|
+
return { allowedRegistryHosts: [], sharedPath: null, auditSettings: {}, exceptions: [], exclude: [], failOn: null, warnings: [] };
|
|
314
|
+
}
|
|
187
315
|
|
|
188
316
|
let raw;
|
|
189
317
|
try {
|
|
@@ -199,23 +327,31 @@ export function loadSharedConfig(cwd = process.cwd()) {
|
|
|
199
327
|
throw new AuditConfigError(`Invalid JSON in ${sharedPath}: ${e.message}`, 'SHARED_CONFIG_PARSE', { sharedPath });
|
|
200
328
|
}
|
|
201
329
|
|
|
330
|
+
validateSharedShape(parsed, sharedPath);
|
|
331
|
+
const warnings = filenameWarnings(sharedPath);
|
|
332
|
+
const settings = resolveToolSection(parsed, warnings);
|
|
333
|
+
|
|
202
334
|
return {
|
|
203
335
|
allowedRegistryHosts: collectSharedHosts(parsed),
|
|
204
336
|
sharedPath,
|
|
205
|
-
auditSettings:
|
|
337
|
+
auditSettings: { ...(settings.rules ? { rules: settings.rules } : {}), ...(settings.maxWarnings !== undefined ? { maxWarnings: settings.maxWarnings } : {}) },
|
|
338
|
+
exceptions: settings.exceptions,
|
|
339
|
+
exclude: settings.exclude,
|
|
340
|
+
failOn: settings.failOn || null,
|
|
341
|
+
warnings
|
|
206
342
|
};
|
|
207
343
|
}
|
|
208
344
|
|
|
209
345
|
export function loadAuditConfig(cwd = process.cwd(), explicitPath = null) {
|
|
210
|
-
// The shared `.dependably
|
|
211
|
-
//
|
|
212
|
-
//
|
|
213
|
-
// `.npm-checkrc.json` (or an explicit `--config`) overrides it.
|
|
346
|
+
// The shared `.dependably` (discovered by walking up to the repo root) is the
|
|
347
|
+
// PRIMARY config source. A tool-specific `.npm-checkrc.json` (or an explicit
|
|
348
|
+
// `--config`) overrides it.
|
|
214
349
|
const shared = loadSharedConfig(cwd);
|
|
215
350
|
|
|
216
351
|
let toolConfig = {};
|
|
217
352
|
let configPath = null;
|
|
218
353
|
let explicitSharedHosts = [];
|
|
354
|
+
let explicitExtras = null; // { exceptions, exclude, failOn } from an explicit shared-shape file
|
|
219
355
|
|
|
220
356
|
if (explicitPath) {
|
|
221
357
|
configPath = path.resolve(explicitPath);
|
|
@@ -224,17 +360,20 @@ export function loadAuditConfig(cwd = process.cwd(), explicitPath = null) {
|
|
|
224
360
|
}
|
|
225
361
|
const parsed = readJsonConfig(configPath);
|
|
226
362
|
if (isSharedShape(configPath, parsed)) {
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
363
|
+
validateSharedShape(parsed, configPath);
|
|
364
|
+
const warnings = [];
|
|
365
|
+
const settings = resolveToolSection(parsed, warnings);
|
|
366
|
+
shared.warnings.push(...warnings);
|
|
367
|
+
toolConfig = { ...(settings.rules ? { rules: settings.rules } : {}), ...(settings.maxWarnings !== undefined ? { maxWarnings: settings.maxWarnings } : {}) };
|
|
230
368
|
explicitSharedHosts = collectSharedHosts(parsed);
|
|
369
|
+
explicitExtras = { exceptions: settings.exceptions, exclude: settings.exclude, failOn: settings.failOn || null };
|
|
231
370
|
} else {
|
|
232
371
|
// Legacy flat tool-config (.npm-checkrc.json shape) given explicitly.
|
|
233
372
|
toolConfig = parsed;
|
|
234
373
|
}
|
|
235
374
|
} else {
|
|
236
375
|
// Discover a tool-specific config in the working directory (fallback for
|
|
237
|
-
// back-compat; the shared `.dependably
|
|
376
|
+
// back-compat; the shared `.dependably` above is the primary source).
|
|
238
377
|
for (const name of CONFIG_FILENAMES) {
|
|
239
378
|
const candidate = path.join(cwd, name);
|
|
240
379
|
if (fs.existsSync(candidate)) {
|
|
@@ -249,13 +388,29 @@ export function loadAuditConfig(cwd = process.cwd(), explicitPath = null) {
|
|
|
249
388
|
const userConfig = { ...shared.auditSettings, ...toolConfig };
|
|
250
389
|
const config = mergeConfig(userConfig, configPath || shared.sharedPath);
|
|
251
390
|
|
|
252
|
-
// Layer the shared
|
|
253
|
-
//
|
|
254
|
-
// tool-config replacement) — public npm always stays trusted.
|
|
391
|
+
// Layer the shared allowlist ADDITIVELY onto secure-resolved / no-remote-deps
|
|
392
|
+
// (public npm always stays trusted).
|
|
255
393
|
const hosts = [...new Set([...shared.allowedRegistryHosts, ...explicitSharedHosts])];
|
|
256
394
|
if (hosts.length > 0) {
|
|
257
395
|
extendAllowedHosts(config, hosts);
|
|
258
396
|
}
|
|
397
|
+
|
|
398
|
+
// failOn.count is the standard form of maxWarnings; a legacy maxWarnings in a
|
|
399
|
+
// tool-config still wins if it was set (it flowed through mergeConfig above).
|
|
400
|
+
const failOn = explicitExtras ? explicitExtras.failOn : shared.failOn;
|
|
401
|
+
if (failOn) {
|
|
402
|
+
if (failOn.count !== undefined && toolConfig.maxWarnings === undefined && shared.auditSettings.maxWarnings === undefined) {
|
|
403
|
+
if (typeof failOn.count !== 'number' || !Number.isInteger(failOn.count) || failOn.count < 0) {
|
|
404
|
+
throw new AuditConfigError(`failOn.count must be a non-negative integer, got: ${JSON.stringify(failOn.count)}`, 'INVALID_FAIL_ON');
|
|
405
|
+
}
|
|
406
|
+
config.maxWarnings = failOn.count;
|
|
407
|
+
}
|
|
408
|
+
config.failOnSeverity = failOn.severity || null;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
config.exceptions = explicitExtras ? explicitExtras.exceptions : shared.exceptions;
|
|
412
|
+
config.exclude = explicitExtras ? explicitExtras.exclude : shared.exclude;
|
|
413
|
+
config.warnings = shared.warnings;
|
|
259
414
|
config.sharedConfigPath = shared.sharedPath;
|
|
260
415
|
|
|
261
416
|
return config;
|
|
@@ -265,11 +420,9 @@ export function loadAuditConfig(cwd = process.cwd(), explicitPath = null) {
|
|
|
265
420
|
* Add the given hosts to every host-based rule's `allowedHosts`, deduplicated,
|
|
266
421
|
* without replacing the existing entries. No-op for rules that are absent.
|
|
267
422
|
*
|
|
268
|
-
* BOTH `secure-resolved`
|
|
269
|
-
*
|
|
270
|
-
*
|
|
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.
|
|
423
|
+
* BOTH `secure-resolved` and `no-remote-deps` consult `allowedHosts`; the shared
|
|
424
|
+
* allowlist must reach both or a private-registry project silences one rule but
|
|
425
|
+
* still trips the other.
|
|
273
426
|
*
|
|
274
427
|
* @param {object} config - A merged audit config (from mergeConfig)
|
|
275
428
|
* @param {string[]} hosts - Bare hostnames to add to the allowlist
|