@codyswann/lisa 4.24.0 → 4.24.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/all/copy-overwrite/scripts/check-orphaned-branches.mjs +218 -0
- package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
- package/dist/core/lisa-owned-hash-ledger.js +8 -0
- package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
- package/dist/core/nightly-e2e-guard-behavior-certificate.js +2 -2
- package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
- package/dist/core/upstream-evidence-manifest.js +7 -4
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/package.json +3 -3
- package/plugins/lisa/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/skills/lisa-git-submit-pr/SKILL.md +12 -0
- package/plugins/lisa/skills/lisa-git-submit-pr/SKILL.md +12 -0
- package/plugins/lisa-agy/plugin.json +1 -1
- package/plugins/lisa-agy/skills/lisa-git-submit-pr/SKILL.md +12 -0
- package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-agy/plugin.json +1 -1
- package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/skills/lisa-git-submit-pr/SKILL.md +12 -0
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/skills/lisa-git-submit-pr/SKILL.md +12 -0
- package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-agy/plugin.json +1 -1
- package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-agy/plugin.json +1 -1
- package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-agy/plugin.json +1 -1
- package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-agy/plugin.json +1 -1
- package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-agy/plugin.json +1 -1
- package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-agy/plugin.json +1 -1
- package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-agy/plugin.json +1 -1
- package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/src/base/skills/lisa-git-submit-pr/SKILL.md +12 -0
- package/scripts/check-security-floors.mjs +290 -31
- package/scripts/two-channel-couplings.json +1 -1
- package/typescript/copy-overwrite/.prettierignore +1 -0
|
@@ -164,16 +164,21 @@ function trackedManifests() {
|
|
|
164
164
|
|
|
165
165
|
/**
|
|
166
166
|
* Every version constraint declared across the template manifests.
|
|
167
|
-
* @returns {{found: Map<string, Array<{file: string, path: string, spec: string
|
|
167
|
+
* @returns {{found: Map<string, Array<{file: string, path: string, spec: string,
|
|
168
|
+
* lowest: number[]}>>,
|
|
168
169
|
* unresolved: Array<{file: string, path: string, name: string, spec: string}>,
|
|
169
|
-
*
|
|
170
|
-
*
|
|
171
|
-
*
|
|
170
|
+
* unparseable: Array<{file: string, path: string, name: string, spec: string,
|
|
171
|
+
* reason: string}>, scanned: string[], unscanned: string[]|null}}
|
|
172
|
+
* Constraints by package name with their resolved floors, self-references
|
|
173
|
+
* pointing at nothing, floors that resolve to no lower bound, the manifests
|
|
174
|
+
* read, and any tracked manifest the patterns did not reach.
|
|
172
175
|
*/
|
|
173
176
|
export function collectFloors() {
|
|
174
177
|
const found = new Map();
|
|
175
178
|
/** `$name` references pointing at nothing this manifest declares. */
|
|
176
179
|
const unresolved = [];
|
|
180
|
+
/** Floors carrying a version that resolves to no lower bound. */
|
|
181
|
+
const unparseable = [];
|
|
177
182
|
const files = globSync(MANIFEST_PATTERNS).filter(
|
|
178
183
|
file => !NOT_A_MANIFEST.some(fragment => file.includes(fragment))
|
|
179
184
|
);
|
|
@@ -184,6 +189,30 @@ export function collectFloors() {
|
|
|
184
189
|
const scanned = new Set(files);
|
|
185
190
|
const unscanned =
|
|
186
191
|
tracked === null ? null : tracked.filter(file => !scanned.has(file));
|
|
192
|
+
/**
|
|
193
|
+
* File one floor: comparable ones under their package, the rest as gaps.
|
|
194
|
+
*
|
|
195
|
+
* Resolved HERE, at collection, rather than in the audit loop, for two
|
|
196
|
+
* reasons beyond tidiness. It runs before any network call, so a floor with
|
|
197
|
+
* no lower bound is reported even on a run that is entirely rate-limited —
|
|
198
|
+
* the case where a silent skip is hardest to notice. And it makes every site
|
|
199
|
+
* in `found` carry a resolved floor, which removes the audit's null check
|
|
200
|
+
* instead of leaving a `continue` that a later reader has to re-litigate.
|
|
201
|
+
* @param {string} name npm package name.
|
|
202
|
+
* @param {string} file Manifest the floor was declared in.
|
|
203
|
+
* @param {string} path Governance group and section, dotted.
|
|
204
|
+
* @param {string} spec The version constraint to resolve.
|
|
205
|
+
*/
|
|
206
|
+
const record = (name, file, path, spec) => {
|
|
207
|
+
const { version, reason } = resolveLowerBound(spec);
|
|
208
|
+
if (version === null) {
|
|
209
|
+
unparseable.push({ file, path, name, spec, reason });
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
if (!found.has(name)) found.set(name, []);
|
|
213
|
+
found.get(name).push({ file, path, spec, lowest: version });
|
|
214
|
+
};
|
|
215
|
+
|
|
187
216
|
for (const file of files) {
|
|
188
217
|
let manifest;
|
|
189
218
|
try {
|
|
@@ -221,33 +250,20 @@ export function collectFloors() {
|
|
|
221
250
|
continue;
|
|
222
251
|
}
|
|
223
252
|
if (!/\d/.test(target)) continue;
|
|
224
|
-
|
|
225
|
-
found
|
|
226
|
-
.get(name)
|
|
227
|
-
.push({ file, path: `${group}.${section}`, spec: target });
|
|
253
|
+
record(name, file, `${group}.${section}`, target);
|
|
228
254
|
continue;
|
|
229
255
|
}
|
|
230
256
|
// A literal carrying no digits — "workspace:*", "latest" — is not a
|
|
231
|
-
// floor and there is nothing to compare.
|
|
257
|
+
// floor and there is nothing to compare. Kept ahead of `record` so
|
|
258
|
+
// these stay a deliberate non-floor rather than becoming a gap
|
|
259
|
+
// report: they are not failed attempts to state a floor.
|
|
232
260
|
if (!/\d/.test(spec)) continue;
|
|
233
|
-
|
|
234
|
-
found.get(name).push({ file, path: `${group}.${section}`, spec });
|
|
261
|
+
record(name, file, `${group}.${section}`, spec);
|
|
235
262
|
}
|
|
236
263
|
}
|
|
237
264
|
}
|
|
238
265
|
}
|
|
239
|
-
return { found, unresolved, scanned: files, unscanned };
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
/**
|
|
243
|
-
* Lowest version a spec permits, as a comparable tuple.
|
|
244
|
-
* @param {string} spec A range like ">=5.0.7", "^1.2.3", "~2.0.0".
|
|
245
|
-
* @returns {number[]|null} [major, minor, patch], or null if unparseable.
|
|
246
|
-
*/
|
|
247
|
-
export function lowestPermitted(spec) {
|
|
248
|
-
const match = /(\d+)\.(\d+)\.(\d+)/.exec(spec);
|
|
249
|
-
if (!match) return null;
|
|
250
|
-
return match.slice(1, 4).map(Number);
|
|
266
|
+
return { found, unresolved, unparseable, scanned: files, unscanned };
|
|
251
267
|
}
|
|
252
268
|
|
|
253
269
|
/**
|
|
@@ -263,6 +279,200 @@ function compare(left, right) {
|
|
|
263
279
|
return 0;
|
|
264
280
|
}
|
|
265
281
|
|
|
282
|
+
/** The comparator an npm term may lead with. */
|
|
283
|
+
const TERM_OPERATOR = /^(>=|<=|>|<|=|\^|~)/;
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* The version half of a term, anchored end to end.
|
|
287
|
+
*
|
|
288
|
+
* Anchored deliberately. The predecessor was an unanchored
|
|
289
|
+
* `/(\d+)\.(\d+)\.(\d+)/` scan, and a scan cannot tell a version from a number
|
|
290
|
+
* that merely follows one: on `^1.2.3-alpha.4` a global sweep finds `1.2.3`
|
|
291
|
+
* and then finds `4` in the prerelease tag and reads it as a second term. An
|
|
292
|
+
* anchored match either consumes the whole thing or rejects it, which is the
|
|
293
|
+
* property that lets a rejection mean "report this" instead of "guess".
|
|
294
|
+
*
|
|
295
|
+
* The operator and any prerelease or build tag are removed before this runs,
|
|
296
|
+
* rather than being alternatives inside it. Splitting the three concerns keeps
|
|
297
|
+
* each pattern simple enough to read, and the tag is discarded regardless
|
|
298
|
+
* because every comparison in this file is on the [major, minor, patch] tuple.
|
|
299
|
+
*/
|
|
300
|
+
const TERM_VERSION = /^v?(\d+)(?:\.(\d+|[xX*]))?(?:\.(\d+|[xX*]))?$/;
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Lower bound of one comparator term.
|
|
304
|
+
*
|
|
305
|
+
* Three outcomes, kept distinct because collapsing any two of them is how a
|
|
306
|
+
* floor goes unchecked:
|
|
307
|
+
*
|
|
308
|
+
* - `bound` — the term states a lowest permitted version.
|
|
309
|
+
* - `floorless` — the term is an upper bound (`<8`, `<=2.0.0`). It states no
|
|
310
|
+
* floor at all; it permits every release beneath it.
|
|
311
|
+
* - `unreadable` — this check cannot tell which of the two it is.
|
|
312
|
+
* @param {string} term A single comparator, e.g. ">=1.2.3" or "^8".
|
|
313
|
+
* @returns {{kind: string, version?: number[]}} The classification.
|
|
314
|
+
*/
|
|
315
|
+
function termLowerBound(term) {
|
|
316
|
+
const operator = TERM_OPERATOR.exec(term)?.[1] ?? "";
|
|
317
|
+
if (operator === "<" || operator === "<=") return { kind: "floorless" };
|
|
318
|
+
// A prerelease or build tag is the only place a `-` or `+` survives this
|
|
319
|
+
// far: hyphen RANGES were split before the term was formed. Taking the head
|
|
320
|
+
// is what stops `-alpha.4` contributing a `4` to the version.
|
|
321
|
+
const [core] = term.slice(operator.length).split(/[-+]/);
|
|
322
|
+
|
|
323
|
+
const match = TERM_VERSION.exec(core);
|
|
324
|
+
if (!match) return { kind: "unreadable" };
|
|
325
|
+
// `x` and `*` are wildcards, so they and everything after them are
|
|
326
|
+
// unspecified. `1.x.5` is not a version anyone means; truncating at the
|
|
327
|
+
// first wildcard is what makes `1.x` and `1` resolve identically.
|
|
328
|
+
const specified = [];
|
|
329
|
+
for (const part of [match[1], match[2], match[3]]) {
|
|
330
|
+
if (part === undefined || /^[xX*]$/.test(part)) break;
|
|
331
|
+
specified.push(Number(part));
|
|
332
|
+
}
|
|
333
|
+
// An absent minor or patch is zero, not missing: `^8` is `>=8.0.0`. Leaving
|
|
334
|
+
// them undefined would put NaN into the tuple, and NaN compares as neither
|
|
335
|
+
// higher nor lower than anything — it disables the comparison it feeds
|
|
336
|
+
// rather than failing it.
|
|
337
|
+
const lower = [...specified, 0, 0].slice(0, 3);
|
|
338
|
+
if (operator !== ">") return { kind: "bound", version: lower };
|
|
339
|
+
|
|
340
|
+
// npm expands a STRICT comparator before comparing: `>1` is `>=2.0.0` and
|
|
341
|
+
// `>1.2` is `>=1.3.0`. Reading either as merely 1.0.0 or 1.2.0 invents
|
|
342
|
+
// versions the range does not permit, which in this file means comparing a
|
|
343
|
+
// release the floor already excludes.
|
|
344
|
+
const lastSpecified = specified.length - 1;
|
|
345
|
+
lower[lastSpecified] += 1;
|
|
346
|
+
for (let index = lastSpecified + 1; index < 3; index += 1) lower[index] = 0;
|
|
347
|
+
return { kind: "bound", version: lower };
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Lower bound of one `||` branch: the HIGHEST floor its terms state.
|
|
352
|
+
*
|
|
353
|
+
* A branch is a conjunction — `>=1.2.3 <2.0.0` permits a version only if every
|
|
354
|
+
* term admits it — so the binding floor is the strongest one present. Terms
|
|
355
|
+
* that state no floor (`<2.0.0`) constrain the ceiling and are passed over;
|
|
356
|
+
* a branch made only of those has no floor.
|
|
357
|
+
* @param {string} branch One branch of a `||` range.
|
|
358
|
+
* @returns {{kind: string, version?: number[]}} The classification.
|
|
359
|
+
*/
|
|
360
|
+
function branchLowerBound(branch) {
|
|
361
|
+
const trimmed = branch.trim();
|
|
362
|
+
if (trimmed === "") return { kind: "unreadable" };
|
|
363
|
+
// A hyphen range's floor is its LEFT side. Both sides are bare versions, so
|
|
364
|
+
// measuring every term and keeping the highest would return `2.0.0` for
|
|
365
|
+
// `1.2.3 - 2.0.0` — the ceiling reported as the floor, which is the one
|
|
366
|
+
// direction that clears a vulnerable release instead of flagging it.
|
|
367
|
+
const [beneathAnyCeiling] = trimmed.split(/\s+-\s+/);
|
|
368
|
+
// semver permits whitespace between an operator and its version, so `>= 8`
|
|
369
|
+
// is `>=8`. Closing the gap before splitting on whitespace stops a legal
|
|
370
|
+
// range being split into a bare operator plus a bare version — which would
|
|
371
|
+
// read as unreadable and report a floor that is in fact perfectly clear.
|
|
372
|
+
const terms = beneathAnyCeiling
|
|
373
|
+
.replace(/(>=|<=|>|<|=|\^|~)\s+/g, "$1")
|
|
374
|
+
.split(/\s+/)
|
|
375
|
+
.filter(Boolean);
|
|
376
|
+
let strongest = null;
|
|
377
|
+
for (const term of terms) {
|
|
378
|
+
const resolved = termLowerBound(term);
|
|
379
|
+
if (resolved.kind === "unreadable") return { kind: "unreadable" };
|
|
380
|
+
if (resolved.kind === "floorless") continue;
|
|
381
|
+
if (strongest === null || compare(resolved.version, strongest) > 0) {
|
|
382
|
+
strongest = resolved.version;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
return strongest === null
|
|
386
|
+
? { kind: "floorless" }
|
|
387
|
+
: { kind: "bound", version: strongest };
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Lowest version a spec permits, with the reason when there is none.
|
|
392
|
+
*
|
|
393
|
+
* The reason is produced here rather than by a second pass, so the message a
|
|
394
|
+
* reader sees and the decision the audit makes come from the same parse and
|
|
395
|
+
* cannot disagree.
|
|
396
|
+
*
|
|
397
|
+
* Note what `[0, 0, 0]` would mean if this returned it for an upper-bound-only
|
|
398
|
+
* spec, which is how the sibling collision checker in the templates models a
|
|
399
|
+
* floorless range: {@link withinRange} would compare 0.0.0 against an advisory
|
|
400
|
+
* range like `>= 4.0.0, < 5.0.8`, find it outside, and report the floor CLEAN.
|
|
401
|
+
* A spec that permits every vulnerable release would come back green. The two
|
|
402
|
+
* files are therefore deliberately NOT unified — there, zero means "weakest
|
|
403
|
+
* possible, loses every comparison"; here, no comparison at that end exists,
|
|
404
|
+
* so a floorless spec has to be reported instead of compared.
|
|
405
|
+
* @param {string} spec A range like ">=5.0.7", "^8", "~2.0", "^7 || ^8".
|
|
406
|
+
* @returns {{version: number[]|null, reason: string|null}} Bound, or reason.
|
|
407
|
+
*/
|
|
408
|
+
function resolveLowerBound(spec) {
|
|
409
|
+
const raw = typeof spec === "string" ? spec.trim() : "";
|
|
410
|
+
if (raw === "") return { version: null, reason: "is empty" };
|
|
411
|
+
// An alias versions a DIFFERENT package. Its number cannot be compared
|
|
412
|
+
// against advisories filed for this name, and comparing it anyway would
|
|
413
|
+
// answer confidently about the wrong package.
|
|
414
|
+
if (/^npm:/i.test(raw)) {
|
|
415
|
+
return {
|
|
416
|
+
version: null,
|
|
417
|
+
reason:
|
|
418
|
+
"is an alias, so its version belongs to another package and cannot be compared against this one's advisories",
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const branches = raw.split("||").map(branchLowerBound);
|
|
423
|
+
if (branches.some(branch => branch.kind === "unreadable")) {
|
|
424
|
+
return {
|
|
425
|
+
version: null,
|
|
426
|
+
reason: "is not a version range this check knows how to read",
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
// A disjunction permits the union of its branches, so one floorless branch
|
|
430
|
+
// makes the whole spec floorless: `<8 || ^9` permits everything below 8.
|
|
431
|
+
// Filtering that branch out would let the strongest branch speak for the
|
|
432
|
+
// weakest, reporting a floor the spec does not have.
|
|
433
|
+
if (branches.some(branch => branch.kind === "floorless")) {
|
|
434
|
+
return {
|
|
435
|
+
version: null,
|
|
436
|
+
reason:
|
|
437
|
+
"sets only an upper bound, so it permits every earlier release including vulnerable ones",
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
return {
|
|
441
|
+
version: branches.reduce(
|
|
442
|
+
(lowest, branch) =>
|
|
443
|
+
compare(branch.version, lowest) < 0 ? branch.version : lowest,
|
|
444
|
+
branches[0].version
|
|
445
|
+
),
|
|
446
|
+
reason: null,
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Lowest version a spec permits, as a comparable tuple.
|
|
452
|
+
*
|
|
453
|
+
* Partial and wildcard constraints resolve to their floor — `^8` is `8.0.0`,
|
|
454
|
+
* `~8.2` is `8.2.0`, `5.x` is `5.0.0` — rather than to null. Requiring all
|
|
455
|
+
* three components meant every such floor came back null, and the audit's
|
|
456
|
+
* `if (!lowest) continue` then skipped it in silence (lisa#3438).
|
|
457
|
+
*
|
|
458
|
+
* `null` now means only "no floor was read here" and never "nothing to
|
|
459
|
+
* check". Callers must report it; {@link lowerBoundGap} supplies the wording.
|
|
460
|
+
* @param {string} spec A range like ">=5.0.7", "^8", "~2.0", "^7 || ^8".
|
|
461
|
+
* @returns {number[]|null} [major, minor, patch], or null when there is none.
|
|
462
|
+
*/
|
|
463
|
+
export function lowestPermitted(spec) {
|
|
464
|
+
return resolveLowerBound(spec).version;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Why a spec has no comparable floor, in words an operator can act on.
|
|
469
|
+
* @param {string} spec A version constraint.
|
|
470
|
+
* @returns {string|null} The reason, or null when the spec does resolve.
|
|
471
|
+
*/
|
|
472
|
+
export function lowerBoundGap(spec) {
|
|
473
|
+
return resolveLowerBound(spec).reason;
|
|
474
|
+
}
|
|
475
|
+
|
|
266
476
|
/**
|
|
267
477
|
* Whether a version falls inside an advisory's vulnerable range.
|
|
268
478
|
* @param {number[]} version Candidate version.
|
|
@@ -346,10 +556,17 @@ async function advisoriesFor(name) {
|
|
|
346
556
|
/**
|
|
347
557
|
* Audit every collected floor.
|
|
348
558
|
* @returns {Promise<{problems: Array, unreachable: Array, unresolved: Array,
|
|
349
|
-
* unscanned: string[]|null, manifests: number,
|
|
559
|
+
* unparseable: Array, unscanned: string[]|null, manifests: number,
|
|
560
|
+
* checked: number}>} Findings.
|
|
350
561
|
*/
|
|
351
562
|
async function audit() {
|
|
352
|
-
const {
|
|
563
|
+
const {
|
|
564
|
+
found: floors,
|
|
565
|
+
unresolved,
|
|
566
|
+
unparseable,
|
|
567
|
+
scanned,
|
|
568
|
+
unscanned,
|
|
569
|
+
} = collectFloors();
|
|
353
570
|
const problems = [];
|
|
354
571
|
const unreachable = [];
|
|
355
572
|
for (const [name, sites] of floors) {
|
|
@@ -364,9 +581,15 @@ async function audit() {
|
|
|
364
581
|
for (const vulnerability of advisory.vulnerabilities ?? []) {
|
|
365
582
|
if (vulnerability.package?.name !== name) continue;
|
|
366
583
|
for (const site of sites) {
|
|
367
|
-
|
|
368
|
-
if (!lowest) continue
|
|
369
|
-
|
|
584
|
+
// No null check here, deliberately. This loop used to open with
|
|
585
|
+
// `const lowest = lowestPermitted(site.spec); if (!lowest) continue;`
|
|
586
|
+
// — the skip that made an unreadable floor indistinguishable from a
|
|
587
|
+
// floor that passed. Every site in `found` now carries a floor
|
|
588
|
+
// resolved at collection time, and the ones that resolve to nothing
|
|
589
|
+
// are in `unparseable`, where they are reported and gate `--strict`.
|
|
590
|
+
if (
|
|
591
|
+
!withinRange(site.lowest, vulnerability.vulnerable_version_range)
|
|
592
|
+
) {
|
|
370
593
|
continue;
|
|
371
594
|
}
|
|
372
595
|
problems.push({
|
|
@@ -385,6 +608,7 @@ async function audit() {
|
|
|
385
608
|
problems,
|
|
386
609
|
unreachable,
|
|
387
610
|
unresolved,
|
|
611
|
+
unparseable,
|
|
388
612
|
unscanned,
|
|
389
613
|
manifests: scanned.length,
|
|
390
614
|
checked: floors.size,
|
|
@@ -394,13 +618,28 @@ async function audit() {
|
|
|
394
618
|
async function main() {
|
|
395
619
|
const strict = process.argv.includes("--strict");
|
|
396
620
|
const asJson = process.argv.includes("--json");
|
|
397
|
-
const {
|
|
398
|
-
|
|
621
|
+
const {
|
|
622
|
+
problems,
|
|
623
|
+
unreachable,
|
|
624
|
+
unresolved,
|
|
625
|
+
unparseable,
|
|
626
|
+
unscanned,
|
|
627
|
+
manifests,
|
|
628
|
+
checked,
|
|
629
|
+
} = await audit();
|
|
399
630
|
|
|
400
631
|
if (asJson) {
|
|
401
632
|
console.log(
|
|
402
633
|
JSON.stringify(
|
|
403
|
-
{
|
|
634
|
+
{
|
|
635
|
+
problems,
|
|
636
|
+
unreachable,
|
|
637
|
+
unresolved,
|
|
638
|
+
unparseable,
|
|
639
|
+
unscanned,
|
|
640
|
+
manifests,
|
|
641
|
+
checked,
|
|
642
|
+
},
|
|
404
643
|
null,
|
|
405
644
|
2
|
|
406
645
|
)
|
|
@@ -443,6 +682,24 @@ async function main() {
|
|
|
443
682
|
);
|
|
444
683
|
}
|
|
445
684
|
|
|
685
|
+
// A floor whose version cannot be resolved to a lowest permitted release is
|
|
686
|
+
// a floor this audit did not compare. It used to be passed over in silence,
|
|
687
|
+
// which put it in the output as nothing at all — and nothing at all is what
|
|
688
|
+
// a floor that passed also looks like.
|
|
689
|
+
if (!asJson && unparseable.length > 0) {
|
|
690
|
+
console.log(
|
|
691
|
+
`\n> **${unparseable.length} floor(s) could NOT be checked.** Each states a version this audit could not read as a lowest permitted release, so none of them were compared against the advisory database. This is not a clean result for them.`
|
|
692
|
+
);
|
|
693
|
+
for (const entry of unparseable) {
|
|
694
|
+
console.log(
|
|
695
|
+
`> - \`${entry.name}: ${entry.spec}\` in ${entry.file} ${entry.path} — ${entry.reason}`
|
|
696
|
+
);
|
|
697
|
+
}
|
|
698
|
+
console.log(
|
|
699
|
+
"> Fix by rewriting each one as a constraint with a lower bound — `>=x.y.z`, `^x.y.z`, `~x.y.z`, or an exact version."
|
|
700
|
+
);
|
|
701
|
+
}
|
|
702
|
+
|
|
446
703
|
// A manifest the patterns never opened is the same failure as a package the
|
|
447
704
|
// advisory API never answered for: unexamined, and reported clean unless the
|
|
448
705
|
// difference is stated. Printed whether or not anything else went wrong.
|
|
@@ -488,11 +745,13 @@ async function main() {
|
|
|
488
745
|
// does: it is a floor nobody checked, and letting it pass reports it clean.
|
|
489
746
|
// An unscanned tracked manifest gates for the same reason an inconclusive
|
|
490
747
|
// lookup does: it is a floor nobody checked, and passing reports it clean.
|
|
748
|
+
// A floor with no readable lower bound gates for that reason a fourth time.
|
|
491
749
|
if (
|
|
492
750
|
strict &&
|
|
493
751
|
(problems.length > 0 ||
|
|
494
752
|
unreachable.length > 0 ||
|
|
495
753
|
unresolved.length > 0 ||
|
|
754
|
+
unparseable.length > 0 ||
|
|
496
755
|
(unscanned !== null && unscanned.length > 0))
|
|
497
756
|
) {
|
|
498
757
|
process.exitCode = 1;
|