sentinello 3.3.1 → 3.4.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.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/dist/cli.cjs +214 -131
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  <!-- Absolute URLs throughout: npm does not resolve relative links, and this file ships in the package. -->
4
4
  <p>
5
5
  <a href="https://github.com/walkofcode/sentinello/actions/workflows/ci.yml"><img src="https://github.com/walkofcode/sentinello/actions/workflows/ci.yml/badge.svg" alt="CI status" /></a>
6
- <a href="https://github.com/walkofcode/sentinello/blob/main/CONTRIBUTING.md#tests"><img src="https://img.shields.io/badge/coverage-99%25-brightgreen" alt="Statement coverage 99%, enforced by CI" /></a>
6
+ <a href="https://github.com/walkofcode/sentinello/blob/main/CONTRIBUTING.md#tests"><img src="https://img.shields.io/badge/coverage-100%25-brightgreen" alt="100% statement, branch, function and line coverage, enforced by CI" /></a>
7
7
  <img src="https://img.shields.io/badge/license-MIT-blue" alt="MIT licensed" />
8
8
  </p>
9
9
 
package/dist/cli.cjs CHANGED
@@ -1525,14 +1525,14 @@ var require_min_version = __commonJS({
1525
1525
  var require_valid2 = __commonJS({
1526
1526
  "../../node_modules/.pnpm/semver@7.7.4/node_modules/semver/ranges/valid.js"(exports2, module2) {
1527
1527
  var Range2 = require_range();
1528
- var validRange2 = (range, options) => {
1528
+ var validRange3 = (range, options) => {
1529
1529
  try {
1530
1530
  return new Range2(range, options).range || "*";
1531
1531
  } catch (er) {
1532
1532
  return null;
1533
1533
  }
1534
1534
  };
1535
- module2.exports = validRange2;
1535
+ module2.exports = validRange3;
1536
1536
  }
1537
1537
  });
1538
1538
 
@@ -1882,7 +1882,7 @@ var require_semver2 = __commonJS({
1882
1882
  var maxSatisfying = require_max_satisfying();
1883
1883
  var minSatisfying = require_min_satisfying();
1884
1884
  var minVersion = require_min_version();
1885
- var validRange2 = require_valid2();
1885
+ var validRange3 = require_valid2();
1886
1886
  var outside = require_outside();
1887
1887
  var gtr = require_gtr();
1888
1888
  var ltr = require_ltr();
@@ -1920,7 +1920,7 @@ var require_semver2 = __commonJS({
1920
1920
  maxSatisfying,
1921
1921
  minSatisfying,
1922
1922
  minVersion,
1923
- validRange: validRange2,
1923
+ validRange: validRange3,
1924
1924
  outside,
1925
1925
  gtr,
1926
1926
  ltr,
@@ -13152,9 +13152,19 @@ function isSourceUnavailableReason(code) {
13152
13152
  return SOURCE_UNAVAILABLE_REASON_CODES.includes(code);
13153
13153
  }
13154
13154
 
13155
+ // ../../packages/core/src/errors.ts
13156
+ function errText(err) {
13157
+ if (err instanceof Error) return err.message || String(err);
13158
+ return String(err);
13159
+ }
13160
+ function asError(err) {
13161
+ if (err instanceof Error) return err;
13162
+ return new Error(String(err));
13163
+ }
13164
+
13155
13165
  // ../../packages/core/src/advisory-rows.ts
13156
- var OSV_NORMALIZER_VERSION = 4;
13157
- var GEMNASIUM_NORMALIZER_VERSION = 5;
13166
+ var OSV_NORMALIZER_VERSION = 6;
13167
+ var GEMNASIUM_NORMALIZER_VERSION = 7;
13158
13168
 
13159
13169
  // ../../packages/core/src/advisory-export.ts
13160
13170
  var DEFAULT_EXPORT_PROMPT = `You are helping a development team triage and fix the vulnerabilities listed at the bottom of this document. Treat this as a remediation work list, not a checklist to rubber-stamp. Work in a planning posture from the start: if your tooling has a read-only planning mode (Claude Code's plan mode, for example), enter it now and stay in it until the human has approved the triage below. Nothing in this document authorises you to edit a file before then.
@@ -13421,10 +13431,14 @@ function getEcosystem(id) {
13421
13431
  }
13422
13432
  return null;
13423
13433
  }
13434
+ function cacheEcosystemKey(ecosystem) {
13435
+ const def = getEcosystem(ecosystem);
13436
+ return def ? def.osvEcosystem : ecosystem;
13437
+ }
13424
13438
  var GEMNASIUM_SEED_DOWNLOAD_BYTES = 52 * 1024 * 1024;
13425
13439
 
13426
13440
  // ../../packages/scanners/src/lockfile-cross-check.ts
13427
- var import_semver5 = __toESM(require_semver2());
13441
+ var import_semver7 = __toESM(require_semver2());
13428
13442
 
13429
13443
  // ../../packages/versions/src/range.ts
13430
13444
  function isZeroVersion(version2) {
@@ -13468,7 +13482,7 @@ function aboveLowerBound(installed, range, comparator) {
13468
13482
  }
13469
13483
 
13470
13484
  // ../../packages/versions/src/compare.ts
13471
- __toESM(require_semver2());
13485
+ var import_semver2 = __toESM(require_semver2());
13472
13486
 
13473
13487
  // ../../packages/versions/src/comparators/semver.ts
13474
13488
  var import_semver = __toESM(require_semver2());
@@ -13498,6 +13512,63 @@ var semverComparator = {
13498
13512
  }
13499
13513
  };
13500
13514
 
13515
+ // ../../packages/versions/src/compare.ts
13516
+ function compareVersions(a, b) {
13517
+ const left = normalizeSemver(a);
13518
+ const right = normalizeSemver(b);
13519
+ if (left === null || right === null) {
13520
+ if (left === null && right === null) return a < b ? -1 : a > b ? 1 : 0;
13521
+ return left === null ? -1 : 1;
13522
+ }
13523
+ if ((0, import_semver2.lt)(left, right)) return -1;
13524
+ if ((0, import_semver2.gt)(left, right)) return 1;
13525
+ return 0;
13526
+ }
13527
+ function highestVersion(versions) {
13528
+ let best = null;
13529
+ for (const candidate of versions) {
13530
+ if (best === null || compareVersions(candidate, best) > 0) best = candidate;
13531
+ }
13532
+ return best;
13533
+ }
13534
+
13535
+ // ../../packages/versions/src/dialect.ts
13536
+ var import_semver4 = __toESM(require_semver2());
13537
+ function canonicaliseRange(raw, dialect) {
13538
+ if (dialect !== "npm") return raw;
13539
+ const trimmed = raw.trim();
13540
+ if (!/[0-9]/.test(trimmed)) return null;
13541
+ if (ZERO_EXCLUSIVE_LOWER.test(trimmed)) return raw;
13542
+ const canonical = (0, import_semver4.validRange)(trimmed);
13543
+ if (canonical === null) return raw;
13544
+ if (canonical === "*" || canonical.length === 0) return ">=0";
13545
+ return stripSynthesisedPrereleaseMarker(canonical, raw);
13546
+ }
13547
+ var ZERO_EXCLUSIVE_LOWER = /^>\s*v?0(\.0)*$/i;
13548
+ var SYNTHESISED_PRERELEASE_MARKER = /<([0-9][0-9A-Za-z.+-]*)-0(?![0-9A-Za-z.-])/g;
13549
+ var EXPLICIT_PRERELEASE_ZERO = /[0-9][0-9A-Za-z.+-]*-0(?![0-9A-Za-z.-])/g;
13550
+ function stripSynthesisedPrereleaseMarker(canonical, raw) {
13551
+ const explicit = new Set(raw.match(EXPLICIT_PRERELEASE_ZERO) ?? []);
13552
+ return canonical.replace(SYNTHESISED_PRERELEASE_MARKER, function keepExplicit(whole, base) {
13553
+ return explicit.has(base + "-0") ? whole : "<" + base;
13554
+ });
13555
+ }
13556
+ function canMatchSomething(range) {
13557
+ const inclusiveUpper = range.lastAffected ?? null;
13558
+ const upper = inclusiveUpper !== null ? inclusiveUpper : range.fixed;
13559
+ if (upper === null) return true;
13560
+ if (upper === range.introduced) return inclusiveUpper !== null && range.introducedExclusive !== true;
13561
+ if (inclusiveUpper === null && isZeroVersion(range.introduced) && isZeroVersion(upper)) return false;
13562
+ if (range.type !== "SEMVER") return true;
13563
+ const normalizedUpper = normalizeSemver(upper);
13564
+ const normalizedIntroduced = normalizeSemver(range.introduced);
13565
+ if (normalizedUpper === null || normalizedIntroduced === null) return true;
13566
+ if (normalizedUpper === normalizedIntroduced) {
13567
+ return inclusiveUpper !== null && range.introducedExclusive !== true;
13568
+ }
13569
+ return (0, import_semver4.gt)(normalizedUpper, normalizedIntroduced);
13570
+ }
13571
+
13501
13572
  // ../../packages/versions/src/comparators/pep440.ts
13502
13573
  var PRE_RANK = { a: 0, b: 1, rc: 2 };
13503
13574
  var PEP440_RE = new RegExp(
@@ -13515,13 +13586,9 @@ function parsePep440(raw) {
13515
13586
  const m = PEP440_RE.exec(raw.trim());
13516
13587
  if (!m || !m.groups) return null;
13517
13588
  const g = m.groups;
13518
- if (!g.release) return null;
13519
13589
  const release = g.release.split(".").map(function toInt(s) {
13520
13590
  return parseInt(s, 10);
13521
13591
  });
13522
- if (release.some(function bad(n) {
13523
- return Number.isNaN(n);
13524
- })) return null;
13525
13592
  let pre = null;
13526
13593
  if (g.preL) {
13527
13594
  pre = { letter: foldPreLetter(g.preL), n: g.preN ? parseInt(g.preN, 10) : 0 };
@@ -13577,8 +13644,8 @@ function cmpPre(a, b) {
13577
13644
  const kb = preKind(b);
13578
13645
  if (ka !== kb) return ka < kb ? -1 : 1;
13579
13646
  if (ka !== 0 || !a.pre || !b.pre) return 0;
13580
- const la = PRE_RANK[a.pre.letter] ?? 0;
13581
- const lb = PRE_RANK[b.pre.letter] ?? 0;
13647
+ const la = PRE_RANK[a.pre.letter];
13648
+ const lb = PRE_RANK[b.pre.letter];
13582
13649
  if (la !== lb) return la < lb ? -1 : 1;
13583
13650
  if (a.pre.n !== b.pre.n) return a.pre.n < b.pre.n ? -1 : 1;
13584
13651
  return 0;
@@ -13675,7 +13742,7 @@ function shouldKeep(finding) {
13675
13742
  const installed = finding.installedVersion.trim();
13676
13743
  const range = finding.vulnerableRange.trim();
13677
13744
  if (!installed || !range) return true;
13678
- if ((0, import_semver5.validRange)(range) === null) return true;
13745
+ if ((0, import_semver7.validRange)(range) === null) return true;
13679
13746
  const candidates = installed.split(/,\s*/).map(function trim(s) {
13680
13747
  return s.trim();
13681
13748
  }).filter(Boolean);
@@ -13683,7 +13750,7 @@ function shouldKeep(finding) {
13683
13750
  for (const candidate of candidates) {
13684
13751
  const cleaned = normalizeVersion(candidate);
13685
13752
  if (cleaned === null) return true;
13686
- if ((0, import_semver5.satisfies)(cleaned, range, { includePrerelease: true })) return true;
13753
+ if ((0, import_semver7.satisfies)(cleaned, range, { includePrerelease: true })) return true;
13687
13754
  }
13688
13755
  return false;
13689
13756
  }
@@ -28172,7 +28239,7 @@ function date4(params) {
28172
28239
  config(en_default());
28173
28240
 
28174
28241
  // ../../packages/scanners/src/version-fix.ts
28175
- var import_semver6 = __toESM(require_semver2());
28242
+ var import_semver8 = __toESM(require_semver2());
28176
28243
  var VERSION_LITERAL_RE = /\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?/g;
28177
28244
  function parseRangeSafely(input) {
28178
28245
  if (!input) return null;
@@ -28180,7 +28247,7 @@ function parseRangeSafely(input) {
28180
28247
  if (!trimmed) return null;
28181
28248
  if (trimmed === "<0.0.0") return null;
28182
28249
  try {
28183
- return new import_semver6.Range(trimmed, { includePrerelease: false });
28250
+ return new import_semver8.Range(trimmed, { includePrerelease: false });
28184
28251
  } catch {
28185
28252
  return null;
28186
28253
  }
@@ -28191,13 +28258,12 @@ function extractLiteralCandidates(input) {
28191
28258
  if (!matches) return [];
28192
28259
  const out = [];
28193
28260
  for (const m of matches) {
28194
- if ((0, import_semver6.valid)(m)) out.push(m);
28261
+ if ((0, import_semver8.valid)(m)) out.push(m);
28195
28262
  }
28196
28263
  return out;
28197
28264
  }
28198
28265
  function bumpPatch(v) {
28199
- const sv = (0, import_semver6.coerce)(v);
28200
- if (!sv) return null;
28266
+ const sv = (0, import_semver8.coerce)(v);
28201
28267
  return sv.major + "." + sv.minor + "." + (sv.patch + 1);
28202
28268
  }
28203
28269
  function extractRangeLowerBounds(range) {
@@ -28207,13 +28273,12 @@ function extractRangeLowerBounds(range) {
28207
28273
  for (const c of conjuncts) {
28208
28274
  if (!c.semver || !c.semver.version) continue;
28209
28275
  const v = c.semver.version;
28210
- if (!(0, import_semver6.valid)(v)) continue;
28211
28276
  const op = c.operator;
28212
28277
  if (op === ">=" || op === "=" || op === "") {
28213
- if (!candidate || (0, import_semver6.gt)(v, candidate)) candidate = v;
28278
+ if (!candidate || (0, import_semver8.gt)(v, candidate)) candidate = v;
28214
28279
  } else if (op === ">") {
28215
28280
  const inc = bumpPatch(v);
28216
- if (inc && (!candidate || (0, import_semver6.gt)(inc, candidate))) candidate = inc;
28281
+ if (!candidate || (0, import_semver8.gt)(inc, candidate)) candidate = inc;
28217
28282
  }
28218
28283
  }
28219
28284
  if (candidate) out.push(candidate);
@@ -28226,13 +28291,11 @@ function extractRangeUpperBoundsBeyond(range) {
28226
28291
  for (const c of conjuncts) {
28227
28292
  if (!c.semver || !c.semver.version) continue;
28228
28293
  const v = c.semver.version;
28229
- if (!(0, import_semver6.valid)(v)) continue;
28230
28294
  const op = c.operator;
28231
28295
  if (op === "<") {
28232
28296
  out.push(v);
28233
28297
  } else if (op === "<=") {
28234
- const inc = bumpPatch(v);
28235
- if (inc) out.push(inc);
28298
+ out.push(bumpPatch(v));
28236
28299
  }
28237
28300
  }
28238
28301
  }
@@ -28245,8 +28308,8 @@ function pickHighestInstalled(installed) {
28245
28308
  for (const raw of parts) {
28246
28309
  const part = raw.trim();
28247
28310
  if (!part) continue;
28248
- if (!(0, import_semver6.valid)(part)) continue;
28249
- if (!highest || (0, import_semver6.gt)(part, highest)) highest = part;
28311
+ if (!(0, import_semver8.valid)(part)) continue;
28312
+ if (!highest || (0, import_semver8.gt)(part, highest)) highest = part;
28250
28313
  }
28251
28314
  return highest;
28252
28315
  }
@@ -28268,10 +28331,10 @@ function pickSafeFixVersion(args) {
28268
28331
  if (candidates.size === 0) return null;
28269
28332
  let best = null;
28270
28333
  for (const v of candidates) {
28271
- if (patchedRange && !(0, import_semver6.satisfies)(v, patchedRange)) continue;
28272
- if (vulnRange && (0, import_semver6.satisfies)(v, vulnRange)) continue;
28273
- if (installedFloor && !(0, import_semver6.gte)(v, installedFloor)) continue;
28274
- if (!best || (0, import_semver6.gt)(best, v)) best = v;
28334
+ if (patchedRange && !(0, import_semver8.satisfies)(v, patchedRange)) continue;
28335
+ if (vulnRange && (0, import_semver8.satisfies)(v, vulnRange)) continue;
28336
+ if (installedFloor && !(0, import_semver8.gte)(v, installedFloor)) continue;
28337
+ if (!best || (0, import_semver8.gt)(best, v)) best = v;
28275
28338
  }
28276
28339
  return best;
28277
28340
  }
@@ -28390,17 +28453,15 @@ function classifyPackageManagerNotFound(stderr, packageManager) {
28390
28453
  return re1.test(stderr) || re2.test(stderr);
28391
28454
  }
28392
28455
  function pickAuditCommand(lockfile) {
28393
- if (lockfile.packageManager === "pnpm") return "pnpm audit --json";
28394
- if (lockfile.packageManager === "npm") return "npm audit --json";
28395
- return "yarn npm audit --json";
28456
+ if (lockfile.packageManager === "pnpm") return ["pnpm", "audit", "--json"];
28457
+ if (lockfile.packageManager === "npm") return ["npm", "audit", "--json"];
28458
+ return ["yarn", "npm", "audit", "--json"];
28396
28459
  }
28397
28460
  function parseYarnMajor(value) {
28398
28461
  if (typeof value !== "string") return null;
28399
28462
  const match = value.trim().match(/^yarn@(\d+)(?:\.|$)/);
28400
28463
  if (!match || !match[1]) return null;
28401
- const parsed = parseInt(match[1], 10);
28402
- if (Number.isNaN(parsed)) return null;
28403
- return parsed;
28464
+ return parseInt(match[1], 10);
28404
28465
  }
28405
28466
  function unauditableResult(reasonCode, reason, startedAt, rawJson) {
28406
28467
  return {
@@ -28544,9 +28605,7 @@ function normalizeAuditOutput(parsed, installedVersions, classifier) {
28544
28605
  const findings = [];
28545
28606
  let hadVulnerabilityWithoutConcreteAdvisory = false;
28546
28607
  const vulns = parsed.vulnerabilities ?? {};
28547
- for (const packageName of Object.keys(vulns)) {
28548
- const vuln = vulns[packageName];
28549
- if (!vuln) continue;
28608
+ for (const [packageName, vuln] of Object.entries(vulns)) {
28550
28609
  if (!vuln.via || vuln.via.length === 0) {
28551
28610
  hadVulnerabilityWithoutConcreteAdvisory = true;
28552
28611
  continue;
@@ -28694,10 +28753,8 @@ async function loadLockfileSnapshot(projectPath, lockfile) {
28694
28753
  if (!validation.success) return empty;
28695
28754
  const installedVersions = /* @__PURE__ */ new Map();
28696
28755
  const packages = validation.data.packages || {};
28697
- for (const nodePath of Object.keys(packages)) {
28756
+ for (const [nodePath, entry] of Object.entries(packages)) {
28698
28757
  if (!nodePath) continue;
28699
- const entry = packages[nodePath];
28700
- if (!entry) continue;
28701
28758
  if (entry.version) {
28702
28759
  installedVersions.set(nodePath, entry.version);
28703
28760
  }
@@ -28849,9 +28906,6 @@ function executeSpawnAndCapture(input, resolve5) {
28849
28906
  child.stderr.on("data", onSpawnStderrData.bind(null, state));
28850
28907
  child.on("error", onSpawnError.bind(null, ctx));
28851
28908
  child.on("close", onSpawnClose.bind(null, ctx));
28852
- if (input.opts.stdin && child.stdin) {
28853
- child.stdin.end(input.opts.stdin);
28854
- }
28855
28909
  }
28856
28910
  function spawnAndCapture(deps, cmd, args, opts) {
28857
28911
  return new Promise(executeSpawnAndCapture.bind(null, { deps, cmd, args, opts }));
@@ -28911,17 +28965,13 @@ async function runNpmAudit(projectPath, ctx, deps = REAL_DEPS) {
28911
28965
  let execCmd;
28912
28966
  let execArgs;
28913
28967
  if (wrapWithNvm) {
28914
- const wrapped = buildBashWrappedCommand(rawCmd);
28968
+ const wrapped = buildBashWrappedCommand(rawCmd.join(" "));
28915
28969
  execCmd = wrapped.cmd;
28916
28970
  execArgs = wrapped.args;
28917
28971
  } else {
28918
- const parts = rawCmd.split(" ");
28919
- const head = parts[0];
28920
- if (!head) {
28921
- return errorResult("audit_unknown_failure", "failed to construct audit command", startedAt, "");
28922
- }
28972
+ const [head, ...rest] = rawCmd;
28923
28973
  execCmd = head;
28924
- execArgs = parts.slice(1);
28974
+ execArgs = rest;
28925
28975
  }
28926
28976
  const spawnResult = await spawnAndCapture(deps, execCmd, execArgs, {
28927
28977
  cwd: projectPath,
@@ -28967,11 +29017,7 @@ async function runNpmAudit(projectPath, ctx, deps = REAL_DEPS) {
28967
29017
  try {
28968
29018
  parsedJson = JSON.parse(rawText);
28969
29019
  } catch (err) {
28970
- let reason = "audit JSON parse failed";
28971
- if (err instanceof Error) {
28972
- reason = `audit JSON parse failed: ${err.message}`;
28973
- }
28974
- return errorResult("audit_parse_error", reason, startedAt, rawText);
29020
+ return errorResult("audit_parse_error", `audit JSON parse failed: ${errText(err)}`, startedAt, rawText);
28975
29021
  }
28976
29022
  if (lockfile.packageManager === "pnpm") {
28977
29023
  const pnpmValidation = pnpmAuditSchema.safeParse(parsedJson);
@@ -29035,9 +29081,7 @@ function reachableFrom(roots, adjacency) {
29035
29081
  stack.push(root);
29036
29082
  }
29037
29083
  }
29038
- while (stack.length > 0) {
29039
- const node = stack.pop();
29040
- if (node === void 0) break;
29084
+ for (let node = stack.pop(); node !== void 0; node = stack.pop()) {
29041
29085
  const children = adjacency.get(node);
29042
29086
  if (!children) continue;
29043
29087
  for (const child of children) {
@@ -32387,7 +32431,7 @@ function parsePnpmV9(root) {
32387
32431
  if (isProd) existing.scope.isProd = true;
32388
32432
  if (isDev) existing.scope.isDev = true;
32389
32433
  if (!isOptional) existing.scope.isOptional = false;
32390
- if (!existing.depPaths.includes(key)) existing.depPaths.push(key);
32434
+ existing.depPaths.push(key);
32391
32435
  } else {
32392
32436
  byId.set(id, {
32393
32437
  ecosystem: NPM_ECOSYSTEM2,
@@ -32409,7 +32453,6 @@ function parsePnpmLegacy(root) {
32409
32453
  const parsed = parseDepKey(key);
32410
32454
  if (!parsed) continue;
32411
32455
  const version2 = entry.version || parsed.version;
32412
- if (!version2) continue;
32413
32456
  const isDev = entry.dev === true;
32414
32457
  out.push({
32415
32458
  ecosystem: NPM_ECOSYSTEM2,
@@ -32809,7 +32852,6 @@ function classifySkip(absolutePath, rootPath, layers, excludeMatcher, hasExclude
32809
32852
  for (let i = layers.length - 1; i >= 0; i--) {
32810
32853
  const layer = layers[i];
32811
32854
  const rel = path.relative(layer.baseDir, absolutePath);
32812
- if (!rel || rel.startsWith("..")) continue;
32813
32855
  if (!layer.matcher.ignores(rel) && !layer.matcher.ignores(rel + "/")) continue;
32814
32856
  if (layer.hasSentinelloIgnore && !layer.hasGitignore) return "sentinelloignore";
32815
32857
  if (layer.hasGitignore && !layer.hasSentinelloIgnore) return "gitignore";
@@ -32886,9 +32928,7 @@ function detectPackageManager(dir) {
32886
32928
  return "unknown";
32887
32929
  }
32888
32930
  function basenameOf(dir) {
32889
- const abs = path.resolve(dir);
32890
- const parts = abs.split("/");
32891
- return parts[parts.length - 1] || abs;
32931
+ return path.basename(path.resolve(dir));
32892
32932
  }
32893
32933
  function readNvmrcVersion(dir) {
32894
32934
  const nvmrcPath = path.join(dir, ".nvmrc");
@@ -32906,7 +32946,7 @@ function readGitBranch(dir, rootPath) {
32906
32946
  try {
32907
32947
  const head = fs.readFileSync(path.join(gitDir, "HEAD"), "utf8").trim();
32908
32948
  const ref = head.match(/^ref:\s*refs\/heads\/(.+)$/);
32909
- if (ref && ref[1]) return ref[1].trim() || null;
32949
+ if (ref && ref[1]) return ref[1].trim();
32910
32950
  if (/^[0-9a-f]{40}$/i.test(head)) return head.slice(0, 7);
32911
32951
  return null;
32912
32952
  } catch {
@@ -33057,7 +33097,7 @@ function rangesToDisplay(ranges, exactVersions) {
33057
33097
  const parts = [];
33058
33098
  for (const v of exactVersions) parts.push("=" + v);
33059
33099
  for (const range of ranges) parts.push(formatRange(range, DISPLAY_FORMAT));
33060
- return parts.length > 0 ? parts.join(" || ") : "*";
33100
+ return parts.join(" || ");
33061
33101
  }
33062
33102
  function vulnerableRangeForFix(ranges) {
33063
33103
  return formatRanges(ranges, SEMVER_FORMAT);
@@ -33109,7 +33149,6 @@ function matchPackages(packages, lookup, isSeeded2, isEnabled) {
33109
33149
  if (!comparator) continue;
33110
33150
  const acceptedRangeTypes = acceptedRangeTypesForEcosystem(ecosystem) ?? [];
33111
33151
  const names = uniqueNames(pkgs);
33112
- if (names.length === 0) continue;
33113
33152
  const byPackageRaw = lookup(ecosystem, names);
33114
33153
  const byPackage = /* @__PURE__ */ new Map();
33115
33154
  for (const [name, advisories] of byPackageRaw.entries()) {
@@ -33216,7 +33255,6 @@ function matchPackages2(packages, lookup, isEnabled) {
33216
33255
  const comparator = comparatorForEcosystem(ecosystem);
33217
33256
  if (!comparator) continue;
33218
33257
  const names = uniqueNames2(pkgs);
33219
- if (names.length === 0) continue;
33220
33258
  const byPackageRaw = lookup(ecosystem, names);
33221
33259
  const byPackage = /* @__PURE__ */ new Map();
33222
33260
  for (const [name, advisories] of byPackageRaw.entries()) {
@@ -33345,10 +33383,6 @@ function signalFor(options, fallbackTimeoutMs) {
33345
33383
  if (options && options.abortSignal) return AbortSignal.any([options.abortSignal, timeout]);
33346
33384
  return timeout;
33347
33385
  }
33348
- function errText(err) {
33349
- if (err instanceof Error) return err.message;
33350
- return String(err);
33351
- }
33352
33386
  function isFastTransientStatus(status) {
33353
33387
  if (status === 408 || status === 425 || status === 429) return true;
33354
33388
  return status >= 500 && status < 600;
@@ -33535,7 +33569,7 @@ function normalizeOsvRecord(record2, ecosystem) {
33535
33569
  if (!pkg2 || pkg2.ecosystem !== ecosystem) continue;
33536
33570
  const packageName = typeof pkg2.name === "string" ? pkg2.name : null;
33537
33571
  if (!packageName) continue;
33538
- const ranges = extractRanges(affected.ranges);
33572
+ const ranges = extractRanges(affected.ranges, affected.database_specific?.last_known_affected_version_range, ecosystem);
33539
33573
  const versions = extractVersions(affected.versions);
33540
33574
  const existing = byPackage.get(packageName);
33541
33575
  if (existing) {
@@ -33571,7 +33605,7 @@ function extractVersions(versions) {
33571
33605
  }
33572
33606
  return out;
33573
33607
  }
33574
- function extractRanges(ranges) {
33608
+ function extractRanges(ranges, lastKnownAffected, ecosystem) {
33575
33609
  if (!Array.isArray(ranges)) return [];
33576
33610
  const out = [];
33577
33611
  for (const range of ranges) {
@@ -33584,18 +33618,20 @@ function extractRanges(ranges) {
33584
33618
  if (typeof event.introduced === "string") {
33585
33619
  if (introduced !== null) {
33586
33620
  out.push({ type: type2, introduced, fixed: null, lastAffected });
33587
- lastAffected = null;
33588
33621
  }
33589
- introduced = event.introduced;
33622
+ lastAffected = null;
33623
+ introduced = !usesSemver(type2, ecosystem) || normalizeSemver(event.introduced) !== null ? event.introduced : null;
33590
33624
  continue;
33591
33625
  }
33592
33626
  if (typeof event.fixed === "string" && introduced !== null) {
33627
+ if (usesSemver(type2, ecosystem) && normalizeSemver(event.fixed) === null) continue;
33593
33628
  out.push({ type: type2, introduced, fixed: event.fixed, lastAffected: null });
33594
33629
  introduced = null;
33595
33630
  lastAffected = null;
33596
33631
  continue;
33597
33632
  }
33598
33633
  if (typeof event.last_affected === "string" && introduced !== null) {
33634
+ if (usesSemver(type2, ecosystem) && normalizeSemver(event.last_affected) === null) continue;
33599
33635
  lastAffected = event.last_affected;
33600
33636
  }
33601
33637
  }
@@ -33603,7 +33639,32 @@ function extractRanges(ranges) {
33603
33639
  out.push({ type: type2, introduced, fixed: null, lastAffected });
33604
33640
  }
33605
33641
  }
33606
- return out;
33642
+ const matchable = [];
33643
+ for (const range of out) {
33644
+ if (canRangeMatch(range, ecosystem)) matchable.push(range);
33645
+ }
33646
+ return boundOpenEndedRange(matchable, lastKnownAffected, ecosystem);
33647
+ }
33648
+ function usesSemver(type2, ecosystem) {
33649
+ if (type2 === "SEMVER") return true;
33650
+ return type2 === "ECOSYSTEM" && getEcosystem(ecosystem)?.comparator === "semver";
33651
+ }
33652
+ function canRangeMatch(range, ecosystem) {
33653
+ return canMatchSomething(usesSemver(range.type, ecosystem) ? { ...range, type: "SEMVER" } : range);
33654
+ }
33655
+ function boundOpenEndedRange(ranges, lastKnownAffected, ecosystem) {
33656
+ if (ranges.length !== 1 || typeof lastKnownAffected !== "string") return ranges;
33657
+ const text = lastKnownAffected.trim();
33658
+ const inclusive = text.startsWith("<=");
33659
+ if (!inclusive && !text.startsWith("<")) return ranges;
33660
+ const version2 = text.slice(inclusive ? 2 : 1).trim();
33661
+ if (version2.length === 0 || /[\s,|]/.test(version2)) return ranges;
33662
+ const range = ranges[0];
33663
+ const open = range.fixed === null && range.lastAffected === null;
33664
+ if (!open) return ranges;
33665
+ if (usesSemver(range.type, ecosystem) && normalizeSemver(version2) === null) return ranges;
33666
+ const bounded = { ...range, fixed: inclusive ? null : version2, lastAffected: inclusive ? version2 : null };
33667
+ return canRangeMatch(bounded, ecosystem) ? [bounded] : ranges;
33607
33668
  }
33608
33669
  function pickSeverity2(r) {
33609
33670
  const ds = r.database_specific && r.database_specific.severity;
@@ -33846,7 +33907,7 @@ function normalizeGemnasiumRecord(record2, ecosystem, slugPrefix) {
33846
33907
  const advisoryId = typeof r.identifier === "string" && r.identifier.length > 0 ? r.identifier : identifiers[0] ?? null;
33847
33908
  if (!advisoryId) return [];
33848
33909
  const fixedVersions = stringArray(r.fixed_versions);
33849
- const parsed = parseAffectedRange(typeof r.affected_range === "string" ? r.affected_range : "", fixedVersions);
33910
+ const parsed = parseAffectedRange(typeof r.affected_range === "string" ? r.affected_range : "", fixedVersions, ecosystem);
33850
33911
  if (parsed.ranges.length === 0 && parsed.versions.length === 0) return [];
33851
33912
  const severity = severityFromCvss(
33852
33913
  typeof r.cvss_v3 === "string" ? r.cvss_v3 : null,
@@ -33891,13 +33952,15 @@ function isRetractedUpstream(record2) {
33891
33952
  return description.includes(phrase);
33892
33953
  });
33893
33954
  }
33894
- function parseAffectedRange(affectedRange, fixedVersions) {
33955
+ function parseAffectedRange(affectedRange, fixedVersions, ecosystem) {
33895
33956
  const ranges = [];
33896
33957
  const versions = [];
33897
33958
  const trimmed = affectedRange.trim();
33898
- const disjuncts = trimmed.length > 0 ? trimmed.split("||").map(trimToken).filter(nonEmpty) : [];
33959
+ const disjuncts = (trimmed.length > 0 ? trimmed.split("||").map(trimToken).filter(nonEmpty) : []).map(function canonical(disjunct) {
33960
+ return canonicaliseRange(disjunct, ecosystem);
33961
+ }).filter(isNotNull).map(trimToken).filter(nonEmpty);
33899
33962
  for (const disjunct of disjuncts) {
33900
- const interval = parseDisjunct(disjunct);
33963
+ const interval = parseDisjunct(disjunct, ecosystem);
33901
33964
  if (!interval) continue;
33902
33965
  if (interval.exact !== null) {
33903
33966
  versions.push(interval.exact);
@@ -33905,31 +33968,29 @@ function parseAffectedRange(affectedRange, fixedVersions) {
33905
33968
  ranges.push(toRange(interval.introduced, interval.introducedExclusive, interval.fixed, interval.lastAffected));
33906
33969
  }
33907
33970
  }
33908
- if (versions.length === 0 && ranges.every(isEmptyInterval)) return { ranges: [], versions: [] };
33971
+ if (versions.length === 0 && !ranges.some(canMatchSomething)) return { ranges: [], versions: [] };
33909
33972
  const only = ranges[0];
33910
33973
  const authoritativeFixed = fixedVersions[0];
33911
- if (ranges.length === 1 && versions.length === 0 && only && fixedVersions.length === 1 && authoritativeFixed !== void 0) {
33974
+ if (ranges.length === 1 && versions.length === 0 && only && fixedVersions.length === 1 && authoritativeFixed !== void 0 && compareVersions(authoritativeFixed, only.introduced) > 0) {
33912
33975
  ranges[0] = toRange(only.introduced, only.introducedExclusive === true, authoritativeFixed, null);
33913
33976
  }
33914
- return { ranges, versions };
33977
+ const highestFixed = highestVersion(fixedVersions);
33978
+ const bounded = [];
33979
+ for (const range of ranges) {
33980
+ const openEnded = range.fixed === null && range.lastAffected === null;
33981
+ bounded.push(highestFixed !== null && openEnded && compareVersions(highestFixed, range.introduced) > 0 ? toRange(range.introduced, range.introducedExclusive === true, highestFixed, null) : range);
33982
+ }
33983
+ return { ranges: bounded, versions };
33915
33984
  }
33916
33985
  function toRange(introduced, introducedExclusive, fixed, lastAffected) {
33917
33986
  const range = { introduced, fixed, lastAffected };
33918
33987
  if (introducedExclusive) range.introducedExclusive = true;
33919
33988
  return range;
33920
33989
  }
33921
- function isEmptyInterval(range) {
33922
- if (range.lastAffected !== null) {
33923
- return range.introducedExclusive === true && range.lastAffected === range.introduced;
33924
- }
33925
- if (range.fixed === null) return false;
33926
- if (range.fixed === range.introduced) return true;
33927
- return isZeroVersion(range.introduced) && isZeroVersion(range.fixed);
33928
- }
33929
- function parseDisjunct(disjunct) {
33990
+ function parseDisjunct(disjunct, ecosystem) {
33930
33991
  const first = disjunct[0];
33931
33992
  if (first === "(" || first === "[") return parseIntervalNotation(disjunct);
33932
- return parseComparatorForm(disjunct);
33993
+ return parseComparatorForm(disjunct, ecosystem);
33933
33994
  }
33934
33995
  function parseIntervalNotation(disjunct) {
33935
33996
  const open = disjunct[0];
@@ -33944,6 +34005,7 @@ function parseIntervalNotation(disjunct) {
33944
34005
  const lo = inner.slice(0, comma).trim();
33945
34006
  const hi = inner.slice(comma + 1).trim();
33946
34007
  const hasUpper = hi.length > 0;
34008
+ if (lo.length === 0 && !hasUpper) return null;
33947
34009
  return {
33948
34010
  introduced: lo.length > 0 ? lo : "0",
33949
34011
  // An open "(" excludes the lower bound — but only when one was actually given; "(,2.0.0)" has no
@@ -33954,17 +34016,18 @@ function parseIntervalNotation(disjunct) {
33954
34016
  exact: null
33955
34017
  };
33956
34018
  }
33957
- function parseComparatorForm(disjunct) {
33958
- const tokens = disjunct.split(/\s+/).filter(nonEmpty);
33959
- if (tokens.length === 0) return null;
34019
+ function parseComparatorForm(disjunct, ecosystem) {
34020
+ const tokens = bindOperators(disjunct.split(/[\s,]+/).filter(nonEmpty));
34021
+ if (!tokens || tokens.length === 0) return null;
33960
34022
  let introduced = "0";
33961
34023
  let introducedExclusive = false;
33962
34024
  let fixed = null;
33963
34025
  let lastAffected = null;
33964
34026
  for (const token of tokens) {
33965
- const op = readOperator(token);
34027
+ const op = readOperator(token, ecosystem);
33966
34028
  if (!op) return null;
33967
34029
  if (op.operator === "=") {
34030
+ if (tokens.length > 1) return null;
33968
34031
  return { introduced: "0", introducedExclusive: false, fixed: null, lastAffected: null, exact: op.version };
33969
34032
  }
33970
34033
  if (op.operator === ">=" || op.operator === ">") {
@@ -33980,16 +34043,37 @@ function parseComparatorForm(disjunct) {
33980
34043
  }
33981
34044
  return { introduced, introducedExclusive, fixed, lastAffected, exact: null };
33982
34045
  }
34046
+ var NAKED_OPERATOR = /^(>=|<=|>|<|=)$/;
34047
+ function bindOperators(tokens) {
34048
+ const bound = [];
34049
+ let pending = null;
34050
+ for (const token of tokens) {
34051
+ if (pending !== null) {
34052
+ bound.push(pending + token);
34053
+ pending = null;
34054
+ } else if (NAKED_OPERATOR.test(token)) {
34055
+ pending = token;
34056
+ } else {
34057
+ bound.push(token);
34058
+ }
34059
+ }
34060
+ return pending === null ? bound : null;
34061
+ }
33983
34062
  var BARE_VERSION = /^[0-9]+(\.[0-9]+)*([-+][0-9A-Za-z.-]*)?$/;
33984
- function readOperator(token) {
33985
- if (token.startsWith(">=")) return { operator: ">=", version: stripV(token.slice(2)) };
33986
- if (token.startsWith("<=")) return { operator: "<=", version: stripV(token.slice(2)) };
33987
- if (token.startsWith(">")) return { operator: ">", version: stripV(token.slice(1)) };
33988
- if (token.startsWith("<")) return { operator: "<", version: stripV(token.slice(1)) };
33989
- if (token.startsWith("=")) return { operator: "=", version: stripV(token.slice(1)) };
34063
+ function readOperator(token, ecosystem) {
34064
+ if (token.startsWith(">=")) return buildOperator(">=", token.slice(2), ecosystem);
34065
+ if (token.startsWith("<=")) return buildOperator("<=", token.slice(2), ecosystem);
34066
+ if (token.startsWith(">")) return buildOperator(">", token.slice(1), ecosystem);
34067
+ if (token.startsWith("<")) return buildOperator("<", token.slice(1), ecosystem);
34068
+ if (token.startsWith("=")) return buildOperator("=", token.slice(1), ecosystem);
33990
34069
  const bare = stripV(token);
33991
34070
  return BARE_VERSION.test(bare) ? { operator: "=", version: bare } : null;
33992
34071
  }
34072
+ function buildOperator(operator, rawVersion, ecosystem) {
34073
+ const version2 = stripV(rawVersion);
34074
+ if (getEcosystem(ecosystem)?.comparator === "semver" && !BARE_VERSION.test(version2)) return null;
34075
+ return { operator, version: version2 };
34076
+ }
33993
34077
  function stripV(raw) {
33994
34078
  const t = raw.trim();
33995
34079
  return t.startsWith("v") || t.startsWith("V") ? t.slice(1) : t;
@@ -34012,6 +34096,9 @@ function trimToken(s) {
34012
34096
  function nonEmpty(s) {
34013
34097
  return s.length > 0;
34014
34098
  }
34099
+ function isNotNull(value) {
34100
+ return value !== null;
34101
+ }
34015
34102
 
34016
34103
  // ../../packages/feeds/src/gemnasium/feed.ts
34017
34104
  var DEFAULT_API_BASE = "https://gitlab.com/api/v4/projects/gitlab-org%2Fsecurity-products%2Fgemnasium-db";
@@ -34106,8 +34193,7 @@ function advisoryIdFromPath(path) {
34106
34193
  const file2 = segments[segments.length - 1];
34107
34194
  if (!file2) return null;
34108
34195
  const dot = file2.lastIndexOf(".");
34109
- const id = dot > 0 ? file2.slice(0, dot) : file2;
34110
- return id.length > 0 ? id : null;
34196
+ return dot > 0 ? file2.slice(0, dot) : file2;
34111
34197
  }
34112
34198
  async function* streamGemnasiumArchive(ref, onProgress, options) {
34113
34199
  const download = await openDownloadStream(archiveUrl(ref), onProgress, options);
@@ -34255,7 +34341,7 @@ function createRowWriter(path) {
34255
34341
  let rowCount = 0;
34256
34342
  let failed = null;
34257
34343
  done.catch(function capture(err) {
34258
- failed = err instanceof Error ? err : new Error(String(err));
34344
+ failed = asError(err);
34259
34345
  });
34260
34346
  async function push(chunk, added) {
34261
34347
  if (failed) throw failed;
@@ -34403,10 +34489,6 @@ async function loadCacheForPackages(cacheDir, ecosystem, packageNames, sources)
34403
34489
  }
34404
34490
  return { osv, gemnasium };
34405
34491
  }
34406
- function cacheEcosystemKey(ecosystem) {
34407
- const def = getEcosystem(ecosystem);
34408
- return def ? def.osvEcosystem : ecosystem;
34409
- }
34410
34492
  function toOsvAdvisory(row) {
34411
34493
  return {
34412
34494
  advisoryId: row.advisoryId,
@@ -34651,15 +34733,18 @@ async function seedGemnasium(options, meta3, item) {
34651
34733
  }
34652
34734
  async function refreshGemnasium(options, meta3, item) {
34653
34735
  const state = getSourceState(meta3, "gemnasium", item.ecosystem);
34654
- const storedSha = state && state.headSha ? state.headSha : null;
34655
- const headSha = await fetchGemnasiumHeadSha(fetchOptionsFor(options, item));
34656
- if (headSha && storedSha === headSha) {
34657
- touch(meta3, "gemnasium", item.ecosystem);
34658
- return { source: "gemnasium", ecosystem: item.ecosystem, status: "unchanged", rowCount: state?.recordCount ?? 0, message: null };
34736
+ if (!state || !state.headSha) {
34737
+ return await seedGemnasium(options, meta3, { ...item, kind: "seed" });
34659
34738
  }
34660
- if (!headSha || !storedSha) {
34739
+ const storedSha = state.headSha;
34740
+ const headSha = await fetchGemnasiumHeadSha(fetchOptionsFor(options, item));
34741
+ if (!headSha) {
34661
34742
  return await seedGemnasium(options, meta3, { ...item, kind: "seed" });
34662
34743
  }
34744
+ if (storedSha === headSha) {
34745
+ touch(meta3, "gemnasium", item.ecosystem);
34746
+ return { source: "gemnasium", ecosystem: item.ecosystem, status: "unchanged", rowCount: state.recordCount, message: null };
34747
+ }
34663
34748
  const changed = await fetchGemnasiumChangedPaths(storedSha, headSha, fetchOptionsFor(options, item));
34664
34749
  if (changed.status === "unavailable") {
34665
34750
  return await seedGemnasium(options, meta3, { ...item, kind: "seed" });
@@ -34671,8 +34756,8 @@ async function refreshGemnasium(options, meta3, item) {
34671
34756
  return pathEcosystemMatches(path, item.ecosystem);
34672
34757
  });
34673
34758
  if (relevant.length === 0 && removed.length === 0) {
34674
- touchGemnasiumSha(meta3, item.ecosystem, headSha);
34675
- return { source: "gemnasium", ecosystem: item.ecosystem, status: "unchanged", rowCount: state?.recordCount ?? 0, message: null };
34759
+ touchGemnasiumSha(state, headSha);
34760
+ return { source: "gemnasium", ecosystem: item.ecosystem, status: "unchanged", rowCount: state.recordCount, message: null };
34676
34761
  }
34677
34762
  const dropAdvisoryIds = /* @__PURE__ */ new Set();
34678
34763
  for (const path of removed.concat(relevant)) {
@@ -34713,9 +34798,7 @@ function touch(meta3, source, ecosystem, etag) {
34713
34798
  state.refreshedAt = Date.now();
34714
34799
  if (etag !== void 0) state.etag = etag;
34715
34800
  }
34716
- function touchGemnasiumSha(meta3, ecosystem, headSha) {
34717
- const state = getSourceState(meta3, "gemnasium", ecosystem);
34718
- if (!state) return;
34801
+ function touchGemnasiumSha(state, headSha) {
34719
34802
  state.refreshedAt = Date.now();
34720
34803
  state.headSha = headSha;
34721
34804
  }
@@ -34944,7 +35027,7 @@ async function applyConfigFile(options, explicitFlags) {
34944
35027
  try {
34945
35028
  parsed = JSON.parse(text);
34946
35029
  } catch (err) {
34947
- return "sentinello.config.json is not valid JSON: " + (err instanceof Error && err.message || String(err));
35030
+ return "sentinello.config.json is not valid JSON: " + errText(err);
34948
35031
  }
34949
35032
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return "sentinello.config.json must contain an object";
34950
35033
  const config2 = parsed;
@@ -35535,8 +35618,8 @@ function createUi(options) {
35535
35618
 
35536
35619
  // src/help.ts
35537
35620
  var CLI_VERSION = (function resolveVersion() {
35538
- if ("3.3.1".length > 0) {
35539
- return "3.3.1";
35621
+ if ("3.4.0".length > 0) {
35622
+ return "3.4.0";
35540
35623
  }
35541
35624
  const fromEnv = process.env.SENTINELLO_VERSION;
35542
35625
  if (fromEnv && fromEnv.trim().length > 0) return fromEnv.trim();
@@ -35814,7 +35897,7 @@ async function runScan(options, cacheDir, ui) {
35814
35897
  try {
35815
35898
  prompt = await resolvePrompt(options);
35816
35899
  } catch (err) {
35817
- ui.error("could not read prompt file: " + (err instanceof Error && err.message || String(err)));
35900
+ ui.error("could not read prompt file: " + errText(err));
35818
35901
  return EXIT_ERROR;
35819
35902
  }
35820
35903
  const document2 = options.json ? renderJson(summary, options, generatedAt) : renderMarkdown(summary, options, prompt, generatedAt);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sentinello",
3
- "version": "3.3.1",
3
+ "version": "3.4.0",
4
4
  "license": "MIT",
5
5
  "description": "Scan the projects in a folder for vulnerable dependencies and write an advisory an agent can act on.",
6
6
  "keywords": [