sentinello 3.3.2 → 3.5.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 +179 -136
  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 = 5;
13157
- var GEMNASIUM_NORMALIZER_VERSION = 6;
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) {
@@ -13518,6 +13532,43 @@ function highestVersion(versions) {
13518
13532
  return best;
13519
13533
  }
13520
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
+
13521
13572
  // ../../packages/versions/src/comparators/pep440.ts
13522
13573
  var PRE_RANK = { a: 0, b: 1, rc: 2 };
13523
13574
  var PEP440_RE = new RegExp(
@@ -13535,13 +13586,9 @@ function parsePep440(raw) {
13535
13586
  const m = PEP440_RE.exec(raw.trim());
13536
13587
  if (!m || !m.groups) return null;
13537
13588
  const g = m.groups;
13538
- if (!g.release) return null;
13539
13589
  const release = g.release.split(".").map(function toInt(s) {
13540
13590
  return parseInt(s, 10);
13541
13591
  });
13542
- if (release.some(function bad(n) {
13543
- return Number.isNaN(n);
13544
- })) return null;
13545
13592
  let pre = null;
13546
13593
  if (g.preL) {
13547
13594
  pre = { letter: foldPreLetter(g.preL), n: g.preN ? parseInt(g.preN, 10) : 0 };
@@ -13597,8 +13644,8 @@ function cmpPre(a, b) {
13597
13644
  const kb = preKind(b);
13598
13645
  if (ka !== kb) return ka < kb ? -1 : 1;
13599
13646
  if (ka !== 0 || !a.pre || !b.pre) return 0;
13600
- const la = PRE_RANK[a.pre.letter] ?? 0;
13601
- const lb = PRE_RANK[b.pre.letter] ?? 0;
13647
+ const la = PRE_RANK[a.pre.letter];
13648
+ const lb = PRE_RANK[b.pre.letter];
13602
13649
  if (la !== lb) return la < lb ? -1 : 1;
13603
13650
  if (a.pre.n !== b.pre.n) return a.pre.n < b.pre.n ? -1 : 1;
13604
13651
  return 0;
@@ -13695,7 +13742,7 @@ function shouldKeep(finding) {
13695
13742
  const installed = finding.installedVersion.trim();
13696
13743
  const range = finding.vulnerableRange.trim();
13697
13744
  if (!installed || !range) return true;
13698
- if ((0, import_semver5.validRange)(range) === null) return true;
13745
+ if ((0, import_semver7.validRange)(range) === null) return true;
13699
13746
  const candidates = installed.split(/,\s*/).map(function trim(s) {
13700
13747
  return s.trim();
13701
13748
  }).filter(Boolean);
@@ -13703,7 +13750,7 @@ function shouldKeep(finding) {
13703
13750
  for (const candidate of candidates) {
13704
13751
  const cleaned = normalizeVersion(candidate);
13705
13752
  if (cleaned === null) return true;
13706
- if ((0, import_semver5.satisfies)(cleaned, range, { includePrerelease: true })) return true;
13753
+ if ((0, import_semver7.satisfies)(cleaned, range, { includePrerelease: true })) return true;
13707
13754
  }
13708
13755
  return false;
13709
13756
  }
@@ -28192,7 +28239,7 @@ function date4(params) {
28192
28239
  config(en_default());
28193
28240
 
28194
28241
  // ../../packages/scanners/src/version-fix.ts
28195
- var import_semver6 = __toESM(require_semver2());
28242
+ var import_semver8 = __toESM(require_semver2());
28196
28243
  var VERSION_LITERAL_RE = /\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?/g;
28197
28244
  function parseRangeSafely(input) {
28198
28245
  if (!input) return null;
@@ -28200,7 +28247,7 @@ function parseRangeSafely(input) {
28200
28247
  if (!trimmed) return null;
28201
28248
  if (trimmed === "<0.0.0") return null;
28202
28249
  try {
28203
- return new import_semver6.Range(trimmed, { includePrerelease: false });
28250
+ return new import_semver8.Range(trimmed, { includePrerelease: false });
28204
28251
  } catch {
28205
28252
  return null;
28206
28253
  }
@@ -28211,13 +28258,12 @@ function extractLiteralCandidates(input) {
28211
28258
  if (!matches) return [];
28212
28259
  const out = [];
28213
28260
  for (const m of matches) {
28214
- if ((0, import_semver6.valid)(m)) out.push(m);
28261
+ if ((0, import_semver8.valid)(m)) out.push(m);
28215
28262
  }
28216
28263
  return out;
28217
28264
  }
28218
28265
  function bumpPatch(v) {
28219
- const sv = (0, import_semver6.coerce)(v);
28220
- if (!sv) return null;
28266
+ const sv = (0, import_semver8.coerce)(v);
28221
28267
  return sv.major + "." + sv.minor + "." + (sv.patch + 1);
28222
28268
  }
28223
28269
  function extractRangeLowerBounds(range) {
@@ -28227,13 +28273,12 @@ function extractRangeLowerBounds(range) {
28227
28273
  for (const c of conjuncts) {
28228
28274
  if (!c.semver || !c.semver.version) continue;
28229
28275
  const v = c.semver.version;
28230
- if (!(0, import_semver6.valid)(v)) continue;
28231
28276
  const op = c.operator;
28232
28277
  if (op === ">=" || op === "=" || op === "") {
28233
- if (!candidate || (0, import_semver6.gt)(v, candidate)) candidate = v;
28278
+ if (!candidate || (0, import_semver8.gt)(v, candidate)) candidate = v;
28234
28279
  } else if (op === ">") {
28235
28280
  const inc = bumpPatch(v);
28236
- if (inc && (!candidate || (0, import_semver6.gt)(inc, candidate))) candidate = inc;
28281
+ if (!candidate || (0, import_semver8.gt)(inc, candidate)) candidate = inc;
28237
28282
  }
28238
28283
  }
28239
28284
  if (candidate) out.push(candidate);
@@ -28246,13 +28291,11 @@ function extractRangeUpperBoundsBeyond(range) {
28246
28291
  for (const c of conjuncts) {
28247
28292
  if (!c.semver || !c.semver.version) continue;
28248
28293
  const v = c.semver.version;
28249
- if (!(0, import_semver6.valid)(v)) continue;
28250
28294
  const op = c.operator;
28251
28295
  if (op === "<") {
28252
28296
  out.push(v);
28253
28297
  } else if (op === "<=") {
28254
- const inc = bumpPatch(v);
28255
- if (inc) out.push(inc);
28298
+ out.push(bumpPatch(v));
28256
28299
  }
28257
28300
  }
28258
28301
  }
@@ -28265,8 +28308,8 @@ function pickHighestInstalled(installed) {
28265
28308
  for (const raw of parts) {
28266
28309
  const part = raw.trim();
28267
28310
  if (!part) continue;
28268
- if (!(0, import_semver6.valid)(part)) continue;
28269
- 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;
28270
28313
  }
28271
28314
  return highest;
28272
28315
  }
@@ -28288,10 +28331,10 @@ function pickSafeFixVersion(args) {
28288
28331
  if (candidates.size === 0) return null;
28289
28332
  let best = null;
28290
28333
  for (const v of candidates) {
28291
- if (patchedRange && !(0, import_semver6.satisfies)(v, patchedRange)) continue;
28292
- if (vulnRange && (0, import_semver6.satisfies)(v, vulnRange)) continue;
28293
- if (installedFloor && !(0, import_semver6.gte)(v, installedFloor)) continue;
28294
- 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;
28295
28338
  }
28296
28339
  return best;
28297
28340
  }
@@ -28410,17 +28453,15 @@ function classifyPackageManagerNotFound(stderr, packageManager) {
28410
28453
  return re1.test(stderr) || re2.test(stderr);
28411
28454
  }
28412
28455
  function pickAuditCommand(lockfile) {
28413
- if (lockfile.packageManager === "pnpm") return "pnpm audit --json";
28414
- if (lockfile.packageManager === "npm") return "npm audit --json";
28415
- 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"];
28416
28459
  }
28417
28460
  function parseYarnMajor(value) {
28418
28461
  if (typeof value !== "string") return null;
28419
28462
  const match = value.trim().match(/^yarn@(\d+)(?:\.|$)/);
28420
28463
  if (!match || !match[1]) return null;
28421
- const parsed = parseInt(match[1], 10);
28422
- if (Number.isNaN(parsed)) return null;
28423
- return parsed;
28464
+ return parseInt(match[1], 10);
28424
28465
  }
28425
28466
  function unauditableResult(reasonCode, reason, startedAt, rawJson) {
28426
28467
  return {
@@ -28564,9 +28605,7 @@ function normalizeAuditOutput(parsed, installedVersions, classifier) {
28564
28605
  const findings = [];
28565
28606
  let hadVulnerabilityWithoutConcreteAdvisory = false;
28566
28607
  const vulns = parsed.vulnerabilities ?? {};
28567
- for (const packageName of Object.keys(vulns)) {
28568
- const vuln = vulns[packageName];
28569
- if (!vuln) continue;
28608
+ for (const [packageName, vuln] of Object.entries(vulns)) {
28570
28609
  if (!vuln.via || vuln.via.length === 0) {
28571
28610
  hadVulnerabilityWithoutConcreteAdvisory = true;
28572
28611
  continue;
@@ -28714,10 +28753,8 @@ async function loadLockfileSnapshot(projectPath, lockfile) {
28714
28753
  if (!validation.success) return empty;
28715
28754
  const installedVersions = /* @__PURE__ */ new Map();
28716
28755
  const packages = validation.data.packages || {};
28717
- for (const nodePath of Object.keys(packages)) {
28756
+ for (const [nodePath, entry] of Object.entries(packages)) {
28718
28757
  if (!nodePath) continue;
28719
- const entry = packages[nodePath];
28720
- if (!entry) continue;
28721
28758
  if (entry.version) {
28722
28759
  installedVersions.set(nodePath, entry.version);
28723
28760
  }
@@ -28869,9 +28906,6 @@ function executeSpawnAndCapture(input, resolve5) {
28869
28906
  child.stderr.on("data", onSpawnStderrData.bind(null, state));
28870
28907
  child.on("error", onSpawnError.bind(null, ctx));
28871
28908
  child.on("close", onSpawnClose.bind(null, ctx));
28872
- if (input.opts.stdin && child.stdin) {
28873
- child.stdin.end(input.opts.stdin);
28874
- }
28875
28909
  }
28876
28910
  function spawnAndCapture(deps, cmd, args, opts) {
28877
28911
  return new Promise(executeSpawnAndCapture.bind(null, { deps, cmd, args, opts }));
@@ -28931,17 +28965,13 @@ async function runNpmAudit(projectPath, ctx, deps = REAL_DEPS) {
28931
28965
  let execCmd;
28932
28966
  let execArgs;
28933
28967
  if (wrapWithNvm) {
28934
- const wrapped = buildBashWrappedCommand(rawCmd);
28968
+ const wrapped = buildBashWrappedCommand(rawCmd.join(" "));
28935
28969
  execCmd = wrapped.cmd;
28936
28970
  execArgs = wrapped.args;
28937
28971
  } else {
28938
- const parts = rawCmd.split(" ");
28939
- const head = parts[0];
28940
- if (!head) {
28941
- return errorResult("audit_unknown_failure", "failed to construct audit command", startedAt, "");
28942
- }
28972
+ const [head, ...rest] = rawCmd;
28943
28973
  execCmd = head;
28944
- execArgs = parts.slice(1);
28974
+ execArgs = rest;
28945
28975
  }
28946
28976
  const spawnResult = await spawnAndCapture(deps, execCmd, execArgs, {
28947
28977
  cwd: projectPath,
@@ -28987,11 +29017,7 @@ async function runNpmAudit(projectPath, ctx, deps = REAL_DEPS) {
28987
29017
  try {
28988
29018
  parsedJson = JSON.parse(rawText);
28989
29019
  } catch (err) {
28990
- let reason = "audit JSON parse failed";
28991
- if (err instanceof Error) {
28992
- reason = `audit JSON parse failed: ${err.message}`;
28993
- }
28994
- return errorResult("audit_parse_error", reason, startedAt, rawText);
29020
+ return errorResult("audit_parse_error", `audit JSON parse failed: ${errText(err)}`, startedAt, rawText);
28995
29021
  }
28996
29022
  if (lockfile.packageManager === "pnpm") {
28997
29023
  const pnpmValidation = pnpmAuditSchema.safeParse(parsedJson);
@@ -29055,9 +29081,7 @@ function reachableFrom(roots, adjacency) {
29055
29081
  stack.push(root);
29056
29082
  }
29057
29083
  }
29058
- while (stack.length > 0) {
29059
- const node = stack.pop();
29060
- if (node === void 0) break;
29084
+ for (let node = stack.pop(); node !== void 0; node = stack.pop()) {
29061
29085
  const children = adjacency.get(node);
29062
29086
  if (!children) continue;
29063
29087
  for (const child of children) {
@@ -32407,7 +32431,7 @@ function parsePnpmV9(root) {
32407
32431
  if (isProd) existing.scope.isProd = true;
32408
32432
  if (isDev) existing.scope.isDev = true;
32409
32433
  if (!isOptional) existing.scope.isOptional = false;
32410
- if (!existing.depPaths.includes(key)) existing.depPaths.push(key);
32434
+ existing.depPaths.push(key);
32411
32435
  } else {
32412
32436
  byId.set(id, {
32413
32437
  ecosystem: NPM_ECOSYSTEM2,
@@ -32429,7 +32453,6 @@ function parsePnpmLegacy(root) {
32429
32453
  const parsed = parseDepKey(key);
32430
32454
  if (!parsed) continue;
32431
32455
  const version2 = entry.version || parsed.version;
32432
- if (!version2) continue;
32433
32456
  const isDev = entry.dev === true;
32434
32457
  out.push({
32435
32458
  ecosystem: NPM_ECOSYSTEM2,
@@ -32829,7 +32852,6 @@ function classifySkip(absolutePath, rootPath, layers, excludeMatcher, hasExclude
32829
32852
  for (let i = layers.length - 1; i >= 0; i--) {
32830
32853
  const layer = layers[i];
32831
32854
  const rel = path.relative(layer.baseDir, absolutePath);
32832
- if (!rel || rel.startsWith("..")) continue;
32833
32855
  if (!layer.matcher.ignores(rel) && !layer.matcher.ignores(rel + "/")) continue;
32834
32856
  if (layer.hasSentinelloIgnore && !layer.hasGitignore) return "sentinelloignore";
32835
32857
  if (layer.hasGitignore && !layer.hasSentinelloIgnore) return "gitignore";
@@ -32906,9 +32928,7 @@ function detectPackageManager(dir) {
32906
32928
  return "unknown";
32907
32929
  }
32908
32930
  function basenameOf(dir) {
32909
- const abs = path.resolve(dir);
32910
- const parts = abs.split("/");
32911
- return parts[parts.length - 1] || abs;
32931
+ return path.basename(path.resolve(dir));
32912
32932
  }
32913
32933
  function readNvmrcVersion(dir) {
32914
32934
  const nvmrcPath = path.join(dir, ".nvmrc");
@@ -32926,7 +32946,7 @@ function readGitBranch(dir, rootPath) {
32926
32946
  try {
32927
32947
  const head = fs.readFileSync(path.join(gitDir, "HEAD"), "utf8").trim();
32928
32948
  const ref = head.match(/^ref:\s*refs\/heads\/(.+)$/);
32929
- if (ref && ref[1]) return ref[1].trim() || null;
32949
+ if (ref && ref[1]) return ref[1].trim();
32930
32950
  if (/^[0-9a-f]{40}$/i.test(head)) return head.slice(0, 7);
32931
32951
  return null;
32932
32952
  } catch {
@@ -33077,7 +33097,7 @@ function rangesToDisplay(ranges, exactVersions) {
33077
33097
  const parts = [];
33078
33098
  for (const v of exactVersions) parts.push("=" + v);
33079
33099
  for (const range of ranges) parts.push(formatRange(range, DISPLAY_FORMAT));
33080
- return parts.length > 0 ? parts.join(" || ") : "*";
33100
+ return parts.join(" || ");
33081
33101
  }
33082
33102
  function vulnerableRangeForFix(ranges) {
33083
33103
  return formatRanges(ranges, SEMVER_FORMAT);
@@ -33129,7 +33149,6 @@ function matchPackages(packages, lookup, isSeeded2, isEnabled) {
33129
33149
  if (!comparator) continue;
33130
33150
  const acceptedRangeTypes = acceptedRangeTypesForEcosystem(ecosystem) ?? [];
33131
33151
  const names = uniqueNames(pkgs);
33132
- if (names.length === 0) continue;
33133
33152
  const byPackageRaw = lookup(ecosystem, names);
33134
33153
  const byPackage = /* @__PURE__ */ new Map();
33135
33154
  for (const [name, advisories] of byPackageRaw.entries()) {
@@ -33236,7 +33255,6 @@ function matchPackages2(packages, lookup, isEnabled) {
33236
33255
  const comparator = comparatorForEcosystem(ecosystem);
33237
33256
  if (!comparator) continue;
33238
33257
  const names = uniqueNames2(pkgs);
33239
- if (names.length === 0) continue;
33240
33258
  const byPackageRaw = lookup(ecosystem, names);
33241
33259
  const byPackage = /* @__PURE__ */ new Map();
33242
33260
  for (const [name, advisories] of byPackageRaw.entries()) {
@@ -33365,10 +33383,6 @@ function signalFor(options, fallbackTimeoutMs) {
33365
33383
  if (options && options.abortSignal) return AbortSignal.any([options.abortSignal, timeout]);
33366
33384
  return timeout;
33367
33385
  }
33368
- function errText(err) {
33369
- if (err instanceof Error) return err.message;
33370
- return String(err);
33371
- }
33372
33386
  function isFastTransientStatus(status) {
33373
33387
  if (status === 408 || status === 425 || status === 429) return true;
33374
33388
  return status >= 500 && status < 600;
@@ -33555,7 +33569,7 @@ function normalizeOsvRecord(record2, ecosystem) {
33555
33569
  if (!pkg2 || pkg2.ecosystem !== ecosystem) continue;
33556
33570
  const packageName = typeof pkg2.name === "string" ? pkg2.name : null;
33557
33571
  if (!packageName) continue;
33558
- const ranges = extractRanges(affected.ranges, affected.database_specific?.last_known_affected_version_range);
33572
+ const ranges = extractRanges(affected.ranges, affected.database_specific?.last_known_affected_version_range, ecosystem);
33559
33573
  const versions = extractVersions(affected.versions);
33560
33574
  const existing = byPackage.get(packageName);
33561
33575
  if (existing) {
@@ -33591,7 +33605,7 @@ function extractVersions(versions) {
33591
33605
  }
33592
33606
  return out;
33593
33607
  }
33594
- function extractRanges(ranges, lastKnownAffected) {
33608
+ function extractRanges(ranges, lastKnownAffected, ecosystem) {
33595
33609
  if (!Array.isArray(ranges)) return [];
33596
33610
  const out = [];
33597
33611
  for (const range of ranges) {
@@ -33604,18 +33618,20 @@ function extractRanges(ranges, lastKnownAffected) {
33604
33618
  if (typeof event.introduced === "string") {
33605
33619
  if (introduced !== null) {
33606
33620
  out.push({ type: type2, introduced, fixed: null, lastAffected });
33607
- lastAffected = null;
33608
33621
  }
33609
- introduced = event.introduced;
33622
+ lastAffected = null;
33623
+ introduced = !usesSemver(type2, ecosystem) || normalizeSemver(event.introduced) !== null ? event.introduced : null;
33610
33624
  continue;
33611
33625
  }
33612
33626
  if (typeof event.fixed === "string" && introduced !== null) {
33627
+ if (usesSemver(type2, ecosystem) && normalizeSemver(event.fixed) === null) continue;
33613
33628
  out.push({ type: type2, introduced, fixed: event.fixed, lastAffected: null });
33614
33629
  introduced = null;
33615
33630
  lastAffected = null;
33616
33631
  continue;
33617
33632
  }
33618
33633
  if (typeof event.last_affected === "string" && introduced !== null) {
33634
+ if (usesSemver(type2, ecosystem) && normalizeSemver(event.last_affected) === null) continue;
33619
33635
  lastAffected = event.last_affected;
33620
33636
  }
33621
33637
  }
@@ -33623,21 +33639,32 @@ function extractRanges(ranges, lastKnownAffected) {
33623
33639
  out.push({ type: type2, introduced, fixed: null, lastAffected });
33624
33640
  }
33625
33641
  }
33626
- return boundOpenEndedRange(out, lastKnownAffected);
33642
+ const matchable = [];
33643
+ for (const range of out) {
33644
+ if (canRangeMatch(range, ecosystem)) matchable.push(range);
33645
+ }
33646
+ return boundOpenEndedRange(matchable, lastKnownAffected, ecosystem);
33627
33647
  }
33628
- function boundOpenEndedRange(ranges, lastKnownAffected) {
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) {
33629
33656
  if (ranges.length !== 1 || typeof lastKnownAffected !== "string") return ranges;
33630
33657
  const text = lastKnownAffected.trim();
33631
33658
  const inclusive = text.startsWith("<=");
33632
33659
  if (!inclusive && !text.startsWith("<")) return ranges;
33633
33660
  const version2 = text.slice(inclusive ? 2 : 1).trim();
33634
33661
  if (version2.length === 0 || /[\s,|]/.test(version2)) return ranges;
33635
- const out = [];
33636
- for (const range of ranges) {
33637
- const open = range.fixed === null && range.lastAffected === null;
33638
- out.push(open ? { ...range, fixed: inclusive ? null : version2, lastAffected: inclusive ? version2 : null } : range);
33639
- }
33640
- return out;
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;
33641
33668
  }
33642
33669
  function pickSeverity2(r) {
33643
33670
  const ds = r.database_specific && r.database_specific.severity;
@@ -33880,7 +33907,7 @@ function normalizeGemnasiumRecord(record2, ecosystem, slugPrefix) {
33880
33907
  const advisoryId = typeof r.identifier === "string" && r.identifier.length > 0 ? r.identifier : identifiers[0] ?? null;
33881
33908
  if (!advisoryId) return [];
33882
33909
  const fixedVersions = stringArray(r.fixed_versions);
33883
- 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);
33884
33911
  if (parsed.ranges.length === 0 && parsed.versions.length === 0) return [];
33885
33912
  const severity = severityFromCvss(
33886
33913
  typeof r.cvss_v3 === "string" ? r.cvss_v3 : null,
@@ -33925,13 +33952,15 @@ function isRetractedUpstream(record2) {
33925
33952
  return description.includes(phrase);
33926
33953
  });
33927
33954
  }
33928
- function parseAffectedRange(affectedRange, fixedVersions) {
33955
+ function parseAffectedRange(affectedRange, fixedVersions, ecosystem) {
33929
33956
  const ranges = [];
33930
33957
  const versions = [];
33931
33958
  const trimmed = affectedRange.trim();
33932
- 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);
33933
33962
  for (const disjunct of disjuncts) {
33934
- const interval = parseDisjunct(disjunct);
33963
+ const interval = parseDisjunct(disjunct, ecosystem);
33935
33964
  if (!interval) continue;
33936
33965
  if (interval.exact !== null) {
33937
33966
  versions.push(interval.exact);
@@ -33939,10 +33968,10 @@ function parseAffectedRange(affectedRange, fixedVersions) {
33939
33968
  ranges.push(toRange(interval.introduced, interval.introducedExclusive, interval.fixed, interval.lastAffected));
33940
33969
  }
33941
33970
  }
33942
- if (versions.length === 0 && ranges.every(isEmptyInterval)) return { ranges: [], versions: [] };
33971
+ if (versions.length === 0 && !ranges.some(canMatchSomething)) return { ranges: [], versions: [] };
33943
33972
  const only = ranges[0];
33944
33973
  const authoritativeFixed = fixedVersions[0];
33945
- 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) {
33946
33975
  ranges[0] = toRange(only.introduced, only.introducedExclusive === true, authoritativeFixed, null);
33947
33976
  }
33948
33977
  const highestFixed = highestVersion(fixedVersions);
@@ -33958,18 +33987,10 @@ function toRange(introduced, introducedExclusive, fixed, lastAffected) {
33958
33987
  if (introducedExclusive) range.introducedExclusive = true;
33959
33988
  return range;
33960
33989
  }
33961
- function isEmptyInterval(range) {
33962
- if (range.lastAffected !== null) {
33963
- return range.introducedExclusive === true && range.lastAffected === range.introduced;
33964
- }
33965
- if (range.fixed === null) return false;
33966
- if (range.fixed === range.introduced) return true;
33967
- return isZeroVersion(range.introduced) && isZeroVersion(range.fixed);
33968
- }
33969
- function parseDisjunct(disjunct) {
33990
+ function parseDisjunct(disjunct, ecosystem) {
33970
33991
  const first = disjunct[0];
33971
33992
  if (first === "(" || first === "[") return parseIntervalNotation(disjunct);
33972
- return parseComparatorForm(disjunct);
33993
+ return parseComparatorForm(disjunct, ecosystem);
33973
33994
  }
33974
33995
  function parseIntervalNotation(disjunct) {
33975
33996
  const open = disjunct[0];
@@ -33984,6 +34005,7 @@ function parseIntervalNotation(disjunct) {
33984
34005
  const lo = inner.slice(0, comma).trim();
33985
34006
  const hi = inner.slice(comma + 1).trim();
33986
34007
  const hasUpper = hi.length > 0;
34008
+ if (lo.length === 0 && !hasUpper) return null;
33987
34009
  return {
33988
34010
  introduced: lo.length > 0 ? lo : "0",
33989
34011
  // An open "(" excludes the lower bound — but only when one was actually given; "(,2.0.0)" has no
@@ -33994,17 +34016,18 @@ function parseIntervalNotation(disjunct) {
33994
34016
  exact: null
33995
34017
  };
33996
34018
  }
33997
- function parseComparatorForm(disjunct) {
33998
- const tokens = disjunct.split(/[\s,]+/).filter(nonEmpty);
33999
- 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;
34000
34022
  let introduced = "0";
34001
34023
  let introducedExclusive = false;
34002
34024
  let fixed = null;
34003
34025
  let lastAffected = null;
34004
34026
  for (const token of tokens) {
34005
- const op = readOperator(token);
34027
+ const op = readOperator(token, ecosystem);
34006
34028
  if (!op) return null;
34007
34029
  if (op.operator === "=") {
34030
+ if (tokens.length > 1) return null;
34008
34031
  return { introduced: "0", introducedExclusive: false, fixed: null, lastAffected: null, exact: op.version };
34009
34032
  }
34010
34033
  if (op.operator === ">=" || op.operator === ">") {
@@ -34020,16 +34043,37 @@ function parseComparatorForm(disjunct) {
34020
34043
  }
34021
34044
  return { introduced, introducedExclusive, fixed, lastAffected, exact: null };
34022
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
+ }
34023
34062
  var BARE_VERSION = /^[0-9]+(\.[0-9]+)*([-+][0-9A-Za-z.-]*)?$/;
34024
- function readOperator(token) {
34025
- if (token.startsWith(">=")) return { operator: ">=", version: stripV(token.slice(2)) };
34026
- if (token.startsWith("<=")) return { operator: "<=", version: stripV(token.slice(2)) };
34027
- if (token.startsWith(">")) return { operator: ">", version: stripV(token.slice(1)) };
34028
- if (token.startsWith("<")) return { operator: "<", version: stripV(token.slice(1)) };
34029
- 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);
34030
34069
  const bare = stripV(token);
34031
34070
  return BARE_VERSION.test(bare) ? { operator: "=", version: bare } : null;
34032
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
+ }
34033
34077
  function stripV(raw) {
34034
34078
  const t = raw.trim();
34035
34079
  return t.startsWith("v") || t.startsWith("V") ? t.slice(1) : t;
@@ -34052,6 +34096,9 @@ function trimToken(s) {
34052
34096
  function nonEmpty(s) {
34053
34097
  return s.length > 0;
34054
34098
  }
34099
+ function isNotNull(value) {
34100
+ return value !== null;
34101
+ }
34055
34102
 
34056
34103
  // ../../packages/feeds/src/gemnasium/feed.ts
34057
34104
  var DEFAULT_API_BASE = "https://gitlab.com/api/v4/projects/gitlab-org%2Fsecurity-products%2Fgemnasium-db";
@@ -34146,8 +34193,7 @@ function advisoryIdFromPath(path) {
34146
34193
  const file2 = segments[segments.length - 1];
34147
34194
  if (!file2) return null;
34148
34195
  const dot = file2.lastIndexOf(".");
34149
- const id = dot > 0 ? file2.slice(0, dot) : file2;
34150
- return id.length > 0 ? id : null;
34196
+ return dot > 0 ? file2.slice(0, dot) : file2;
34151
34197
  }
34152
34198
  async function* streamGemnasiumArchive(ref, onProgress, options) {
34153
34199
  const download = await openDownloadStream(archiveUrl(ref), onProgress, options);
@@ -34295,7 +34341,7 @@ function createRowWriter(path) {
34295
34341
  let rowCount = 0;
34296
34342
  let failed = null;
34297
34343
  done.catch(function capture(err) {
34298
- failed = err instanceof Error ? err : new Error(String(err));
34344
+ failed = asError(err);
34299
34345
  });
34300
34346
  async function push(chunk, added) {
34301
34347
  if (failed) throw failed;
@@ -34443,10 +34489,6 @@ async function loadCacheForPackages(cacheDir, ecosystem, packageNames, sources)
34443
34489
  }
34444
34490
  return { osv, gemnasium };
34445
34491
  }
34446
- function cacheEcosystemKey(ecosystem) {
34447
- const def = getEcosystem(ecosystem);
34448
- return def ? def.osvEcosystem : ecosystem;
34449
- }
34450
34492
  function toOsvAdvisory(row) {
34451
34493
  return {
34452
34494
  advisoryId: row.advisoryId,
@@ -34691,15 +34733,18 @@ async function seedGemnasium(options, meta3, item) {
34691
34733
  }
34692
34734
  async function refreshGemnasium(options, meta3, item) {
34693
34735
  const state = getSourceState(meta3, "gemnasium", item.ecosystem);
34694
- const storedSha = state && state.headSha ? state.headSha : null;
34695
- const headSha = await fetchGemnasiumHeadSha(fetchOptionsFor(options, item));
34696
- if (headSha && storedSha === headSha) {
34697
- touch(meta3, "gemnasium", item.ecosystem);
34698
- 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" });
34699
34738
  }
34700
- if (!headSha || !storedSha) {
34739
+ const storedSha = state.headSha;
34740
+ const headSha = await fetchGemnasiumHeadSha(fetchOptionsFor(options, item));
34741
+ if (!headSha) {
34701
34742
  return await seedGemnasium(options, meta3, { ...item, kind: "seed" });
34702
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
+ }
34703
34748
  const changed = await fetchGemnasiumChangedPaths(storedSha, headSha, fetchOptionsFor(options, item));
34704
34749
  if (changed.status === "unavailable") {
34705
34750
  return await seedGemnasium(options, meta3, { ...item, kind: "seed" });
@@ -34711,8 +34756,8 @@ async function refreshGemnasium(options, meta3, item) {
34711
34756
  return pathEcosystemMatches(path, item.ecosystem);
34712
34757
  });
34713
34758
  if (relevant.length === 0 && removed.length === 0) {
34714
- touchGemnasiumSha(meta3, item.ecosystem, headSha);
34715
- 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 };
34716
34761
  }
34717
34762
  const dropAdvisoryIds = /* @__PURE__ */ new Set();
34718
34763
  for (const path of removed.concat(relevant)) {
@@ -34753,9 +34798,7 @@ function touch(meta3, source, ecosystem, etag) {
34753
34798
  state.refreshedAt = Date.now();
34754
34799
  if (etag !== void 0) state.etag = etag;
34755
34800
  }
34756
- function touchGemnasiumSha(meta3, ecosystem, headSha) {
34757
- const state = getSourceState(meta3, "gemnasium", ecosystem);
34758
- if (!state) return;
34801
+ function touchGemnasiumSha(state, headSha) {
34759
34802
  state.refreshedAt = Date.now();
34760
34803
  state.headSha = headSha;
34761
34804
  }
@@ -34984,7 +35027,7 @@ async function applyConfigFile(options, explicitFlags) {
34984
35027
  try {
34985
35028
  parsed = JSON.parse(text);
34986
35029
  } catch (err) {
34987
- 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);
34988
35031
  }
34989
35032
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return "sentinello.config.json must contain an object";
34990
35033
  const config2 = parsed;
@@ -35575,8 +35618,8 @@ function createUi(options) {
35575
35618
 
35576
35619
  // src/help.ts
35577
35620
  var CLI_VERSION = (function resolveVersion() {
35578
- if ("3.3.2".length > 0) {
35579
- return "3.3.2";
35621
+ if ("3.5.0".length > 0) {
35622
+ return "3.5.0";
35580
35623
  }
35581
35624
  const fromEnv = process.env.SENTINELLO_VERSION;
35582
35625
  if (fromEnv && fromEnv.trim().length > 0) return fromEnv.trim();
@@ -35854,7 +35897,7 @@ async function runScan(options, cacheDir, ui) {
35854
35897
  try {
35855
35898
  prompt = await resolvePrompt(options);
35856
35899
  } catch (err) {
35857
- ui.error("could not read prompt file: " + (err instanceof Error && err.message || String(err)));
35900
+ ui.error("could not read prompt file: " + errText(err));
35858
35901
  return EXIT_ERROR;
35859
35902
  }
35860
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.2",
3
+ "version": "3.5.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": [