sentinello 3.1.0 → 3.2.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 (2) hide show
  1. package/dist/cli.cjs +99 -26
  2. package/package.json +1 -1
package/dist/cli.cjs CHANGED
@@ -13123,6 +13123,19 @@ function compareSeverity(a, b) {
13123
13123
  function meetsSeverityFloor(severity, floor) {
13124
13124
  return severityWeight(severity) >= severityWeight(floor);
13125
13125
  }
13126
+ function maxSeverity(severities) {
13127
+ let best = "info";
13128
+ let bestWeight = 0;
13129
+ for (const s of severities) {
13130
+ const normalized = s.trim().toLowerCase();
13131
+ const known = SEVERITY_WEIGHT[normalized];
13132
+ if (known !== void 0 && known > bestWeight) {
13133
+ bestWeight = known;
13134
+ best = normalized;
13135
+ }
13136
+ }
13137
+ return best;
13138
+ }
13126
13139
  var SOURCE_UNAVAILABLE_REASON_CODES = [
13127
13140
  "osv_db_not_seeded",
13128
13141
  "osv_db_unavailable",
@@ -13134,8 +13147,8 @@ function isSourceUnavailableReason(code) {
13134
13147
  }
13135
13148
 
13136
13149
  // ../../packages/core/src/advisory-rows.ts
13137
- var OSV_NORMALIZER_VERSION = 3;
13138
- var GEMNASIUM_NORMALIZER_VERSION = 2;
13150
+ var OSV_NORMALIZER_VERSION = 4;
13151
+ var GEMNASIUM_NORMALIZER_VERSION = 4;
13139
13152
 
13140
13153
  // ../../packages/core/src/advisory-export.ts
13141
13154
  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.
@@ -33204,22 +33217,51 @@ function findingIdentityKeys(finding) {
33204
33217
  function findingPackageIdentity(finding) {
33205
33218
  return (finding.ecosystem ?? DEFAULT_ECOSYSTEM) + "|" + finding.packageName;
33206
33219
  }
33207
- function reconcileAgainstReported(findings, reportedByPackage) {
33220
+ function reconcileAgainstReported(findings, reportedByPackage, source) {
33208
33221
  const kept = [];
33222
+ const corroborations = [];
33209
33223
  for (const finding of findings) {
33210
33224
  const packageKey = findingPackageIdentity(finding);
33211
- const existing = reportedByPackage.get(packageKey);
33225
+ const existing = reportedByPackage.get(packageKey) || /* @__PURE__ */ new Map();
33212
33226
  const keys = findingIdentityKeys(finding);
33213
- const isDup = existing ? keys.some(function seen(k) {
33214
- return existing.has(k);
33215
- }) : false;
33216
- if (isDup) continue;
33227
+ const target = firstReported(existing, keys);
33228
+ if (target) {
33229
+ const by = { source, advisoryId: finding.advisoryId, severity: finding.severity };
33230
+ corroborations.push({ target, by });
33231
+ const attached = target.finding.corroborations || [];
33232
+ if (!attached.some(function already(c) {
33233
+ return c.source === by.source;
33234
+ })) attached.push(by);
33235
+ target.finding.corroborations = attached;
33236
+ target.finding.severity = escalatedSeverity(target.severity, attached);
33237
+ continue;
33238
+ }
33217
33239
  kept.push(finding);
33218
- const set3 = existing || /* @__PURE__ */ new Set();
33219
- for (const k of keys) set3.add(k);
33220
- reportedByPackage.set(packageKey, set3);
33240
+ const reported = {
33241
+ source,
33242
+ advisoryId: finding.advisoryId,
33243
+ packageName: finding.packageName,
33244
+ ecosystem: finding.ecosystem ?? DEFAULT_ECOSYSTEM,
33245
+ severity: finding.severity,
33246
+ finding
33247
+ };
33248
+ for (const key of keys) existing.set(key, reported);
33249
+ reportedByPackage.set(packageKey, existing);
33221
33250
  }
33222
- return kept;
33251
+ return { kept, corroborations };
33252
+ }
33253
+ function firstReported(existing, keys) {
33254
+ for (const key of keys) {
33255
+ const found = existing.get(key);
33256
+ if (found) return found;
33257
+ }
33258
+ return null;
33259
+ }
33260
+ function escalatedSeverity(own, corroborations) {
33261
+ if (corroborations.length === 0) return own;
33262
+ return maxSeverity([own, ...corroborations.map(function grade(c) {
33263
+ return c.severity;
33264
+ })]);
33223
33265
  }
33224
33266
 
33225
33267
  // ../../packages/scanners/src/index.ts
@@ -33428,18 +33470,21 @@ function normalizeOsvRecord(record2, ecosystem) {
33428
33470
  const url2 = pickUrl(r, advisoryId);
33429
33471
  const withdrawn = typeof r.withdrawn === "string" ? Date.parse(r.withdrawn) || null : null;
33430
33472
  const rows = [];
33431
- const seenPackages = /* @__PURE__ */ new Set();
33473
+ const byPackage = /* @__PURE__ */ new Map();
33432
33474
  for (const affected of r.affected) {
33433
33475
  const pkg2 = affected.package;
33434
33476
  if (!pkg2 || pkg2.ecosystem !== ecosystem) continue;
33435
33477
  const packageName = typeof pkg2.name === "string" ? pkg2.name : null;
33436
33478
  if (!packageName) continue;
33437
- if (seenPackages.has(packageName)) continue;
33438
- seenPackages.add(packageName);
33439
33479
  const ranges = extractRanges(affected.ranges);
33440
33480
  const versions = extractVersions(affected.versions);
33441
- if (ranges.length === 0 && versions.length === 0 && !malicious) continue;
33442
- rows.push({
33481
+ const existing = byPackage.get(packageName);
33482
+ if (existing) {
33483
+ for (const range of ranges) existing.ranges.push(range);
33484
+ for (const version2 of versions) existing.versions.push(version2);
33485
+ continue;
33486
+ }
33487
+ byPackage.set(packageName, {
33443
33488
  advisoryId,
33444
33489
  ecosystem,
33445
33490
  packageName,
@@ -33453,6 +33498,10 @@ function normalizeOsvRecord(record2, ecosystem) {
33453
33498
  withdrawn
33454
33499
  });
33455
33500
  }
33501
+ for (const row of byPackage.values()) {
33502
+ if (row.ranges.length === 0 && row.versions.length === 0 && !malicious) continue;
33503
+ rows.push(row);
33504
+ }
33456
33505
  return rows;
33457
33506
  }
33458
33507
  function extractVersions(versions) {
@@ -33732,6 +33781,7 @@ function normalizeGemnasiumRecord(record2, ecosystem, slugPrefix) {
33732
33781
  if (!slug || !slug.startsWith(slugPrefix)) return [];
33733
33782
  const rawName = slug.slice(slugPrefix.length);
33734
33783
  if (rawName.length === 0) return [];
33784
+ if (isRetractedUpstream(r)) return [];
33735
33785
  const packageName = ecosystem === "PyPI" ? normalizePyName2(rawName) : rawName;
33736
33786
  const identifiers = stringArray(r.identifiers);
33737
33787
  const advisoryId = typeof r.identifier === "string" && r.identifier.length > 0 ? r.identifier : identifiers[0] ?? null;
@@ -33763,16 +33813,30 @@ function normalizeGemnasiumRecord(record2, ecosystem, slugPrefix) {
33763
33813
  withdrawn: null
33764
33814
  }];
33765
33815
  }
33816
+ var RETRACTION_PHRASES = [
33817
+ "marked as false positive",
33818
+ "marked as a false positive",
33819
+ "this is a false positive",
33820
+ "this advisory has been withdrawn",
33821
+ "has been invalidated",
33822
+ "cve being rejected",
33823
+ "cve has been rejected"
33824
+ ];
33825
+ function isRetractedUpstream(record2) {
33826
+ const title = typeof record2.title === "string" ? record2.title.trim().toLowerCase() : "";
33827
+ if (title === "false positive") return true;
33828
+ if (title.startsWith("withdrawn advisory:")) return true;
33829
+ if (title.startsWith("duplicate advisory:")) return true;
33830
+ const description = typeof record2.description === "string" ? record2.description.replace(/\s+/g, " ").toLowerCase() : "";
33831
+ return RETRACTION_PHRASES.some(function stated(phrase) {
33832
+ return description.includes(phrase);
33833
+ });
33834
+ }
33766
33835
  function parseAffectedRange(affectedRange, fixedVersions) {
33767
33836
  const ranges = [];
33768
33837
  const versions = [];
33769
33838
  const trimmed = affectedRange.trim();
33770
33839
  const disjuncts = trimmed.length > 0 ? trimmed.split("||").map(trimToken).filter(nonEmpty) : [];
33771
- if (disjuncts.length === 0) {
33772
- const fixed = fixedVersions[0];
33773
- if (fixed !== void 0) return { ranges: [{ introduced: "0", fixed }], versions };
33774
- return { ranges, versions };
33775
- }
33776
33840
  for (const disjunct of disjuncts) {
33777
33841
  const interval = parseDisjunct(disjunct);
33778
33842
  if (!interval) continue;
@@ -33782,13 +33846,22 @@ function parseAffectedRange(affectedRange, fixedVersions) {
33782
33846
  ranges.push({ introduced: interval.introduced, fixed: interval.fixed });
33783
33847
  }
33784
33848
  }
33849
+ if (versions.length === 0 && ranges.every(isEmptyInterval)) return { ranges: [], versions: [] };
33785
33850
  const only = ranges[0];
33786
33851
  const authoritativeFixed = fixedVersions[0];
33787
- if (ranges.length === 1 && versions.length === 0 && only && authoritativeFixed !== void 0) {
33852
+ if (ranges.length === 1 && versions.length === 0 && only && fixedVersions.length === 1 && authoritativeFixed !== void 0) {
33788
33853
  ranges[0] = { introduced: only.introduced, fixed: authoritativeFixed };
33789
33854
  }
33790
33855
  return { ranges, versions };
33791
33856
  }
33857
+ function isEmptyInterval(range) {
33858
+ if (range.fixed === null) return false;
33859
+ if (range.fixed === range.introduced) return true;
33860
+ return isZeroVersion(range.introduced) && isZeroVersion(range.fixed);
33861
+ }
33862
+ function isZeroVersion(version2) {
33863
+ return /^0(\.0)*$/.test(version2);
33864
+ }
33792
33865
  function parseDisjunct(disjunct) {
33793
33866
  const first = disjunct[0];
33794
33867
  if (first === "(" || first === "[") return parseIntervalNotation(disjunct);
@@ -35051,7 +35124,7 @@ async function scanProject(setup, resolved, scanners) {
35051
35124
  durationMs: result.durationMs
35052
35125
  });
35053
35126
  if (result.status !== "ok") continue;
35054
- for (const finding of reconcileAgainstReported(result.findings, reportedByPackage)) {
35127
+ for (const finding of reconcileAgainstReported(result.findings, reportedByPackage, scanner.name).kept) {
35055
35128
  findings.push(finding);
35056
35129
  }
35057
35130
  }
@@ -35376,8 +35449,8 @@ function createUi(options) {
35376
35449
 
35377
35450
  // src/help.ts
35378
35451
  var CLI_VERSION = (function resolveVersion() {
35379
- if ("3.1.0".length > 0) {
35380
- return "3.1.0";
35452
+ if ("3.2.0".length > 0) {
35453
+ return "3.2.0";
35381
35454
  }
35382
35455
  const fromEnv = process.env.SENTINELLO_VERSION;
35383
35456
  if (fromEnv && fromEnv.trim().length > 0) return fromEnv.trim();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sentinello",
3
- "version": "3.1.0",
3
+ "version": "3.2.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": [