sentinello 3.1.1 → 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 +82 -59
  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",
@@ -13135,7 +13148,7 @@ function isSourceUnavailableReason(code) {
13135
13148
 
13136
13149
  // ../../packages/core/src/advisory-rows.ts
13137
13150
  var OSV_NORMALIZER_VERSION = 4;
13138
- var GEMNASIUM_NORMALIZER_VERSION = 3;
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
@@ -33739,16 +33781,14 @@ function normalizeGemnasiumRecord(record2, ecosystem, slugPrefix) {
33739
33781
  if (!slug || !slug.startsWith(slugPrefix)) return [];
33740
33782
  const rawName = slug.slice(slugPrefix.length);
33741
33783
  if (rawName.length === 0) return [];
33784
+ if (isRetractedUpstream(r)) return [];
33742
33785
  const packageName = ecosystem === "PyPI" ? normalizePyName2(rawName) : rawName;
33743
33786
  const identifiers = stringArray(r.identifiers);
33744
33787
  const advisoryId = typeof r.identifier === "string" && r.identifier.length > 0 ? r.identifier : identifiers[0] ?? null;
33745
33788
  if (!advisoryId) return [];
33746
33789
  const fixedVersions = stringArray(r.fixed_versions);
33747
- const parsed = parseAffectedRange(
33748
- typeof r.affected_range === "string" ? r.affected_range : "",
33749
- fixedVersions,
33750
- typeof r.affected_versions === "string" ? r.affected_versions : ""
33751
- );
33790
+ const parsed = parseAffectedRange(typeof r.affected_range === "string" ? r.affected_range : "", fixedVersions);
33791
+ if (parsed.ranges.length === 0 && parsed.versions.length === 0) return [];
33752
33792
  const severity = severityFromCvss(
33753
33793
  typeof r.cvss_v3 === "string" ? r.cvss_v3 : null,
33754
33794
  typeof r.cvss_v2 === "string" ? r.cvss_v2 : null
@@ -33770,11 +33810,29 @@ function normalizeGemnasiumRecord(record2, ecosystem, slugPrefix) {
33770
33810
  summary,
33771
33811
  url: url2,
33772
33812
  malicious: false,
33773
- withdrawn: null,
33774
- rangeSource: parsed.source
33813
+ withdrawn: null
33775
33814
  }];
33776
33815
  }
33777
- function parseAffectedRange(affectedRange, fixedVersions, affectedVersions = "") {
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
+ }
33835
+ function parseAffectedRange(affectedRange, fixedVersions) {
33778
33836
  const ranges = [];
33779
33837
  const versions = [];
33780
33838
  const trimmed = affectedRange.trim();
@@ -33788,17 +33846,13 @@ function parseAffectedRange(affectedRange, fixedVersions, affectedVersions = "")
33788
33846
  ranges.push({ introduced: interval.introduced, fixed: interval.fixed });
33789
33847
  }
33790
33848
  }
33791
- if (versions.length === 0 && ranges.every(isEmptyInterval)) {
33792
- const prose = parseAffectedVersionsProse(affectedVersions);
33793
- if (prose) return { ranges: prose, versions, source: "prose" };
33794
- return { ranges: [], versions: [], source: "unresolved" };
33795
- }
33849
+ if (versions.length === 0 && ranges.every(isEmptyInterval)) return { ranges: [], versions: [] };
33796
33850
  const only = ranges[0];
33797
33851
  const authoritativeFixed = fixedVersions[0];
33798
33852
  if (ranges.length === 1 && versions.length === 0 && only && fixedVersions.length === 1 && authoritativeFixed !== void 0) {
33799
33853
  ranges[0] = { introduced: only.introduced, fixed: authoritativeFixed };
33800
33854
  }
33801
- return { ranges, versions, source: "range" };
33855
+ return { ranges, versions };
33802
33856
  }
33803
33857
  function isEmptyInterval(range) {
33804
33858
  if (range.fixed === null) return false;
@@ -33808,37 +33862,6 @@ function isEmptyInterval(range) {
33808
33862
  function isZeroVersion(version2) {
33809
33863
  return /^0(\.0)*$/.test(version2);
33810
33864
  }
33811
- function parseAffectedVersionsProse(text) {
33812
- const sentence = text.replace(/\s+/g, " ").trim();
33813
- if (sentence.length === 0) return null;
33814
- const lower = sentence.toLowerCase();
33815
- if (lower === "none" || lower.startsWith("no version") || lower.startsWith("this advisory has been withdrawn")) return null;
33816
- const out = [];
33817
- for (const clause of sentence.split(",")) {
33818
- const c = clause.trim();
33819
- const fromBefore = /^all versions starting from (\S+) before (\S+)$/i.exec(c);
33820
- if (fromBefore && fromBefore[1] && fromBefore[2]) {
33821
- out.push({ introduced: stripV(fromBefore[1]), fixed: stripV(fromBefore[2]) });
33822
- continue;
33823
- }
33824
- const before = /^all versions before (\S+)$/i.exec(c);
33825
- if (before && before[1]) {
33826
- out.push({ introduced: "0", fixed: stripV(before[1]) });
33827
- continue;
33828
- }
33829
- const from = /^all versions starting from (\S+)$/i.exec(c);
33830
- if (from && from[1]) {
33831
- out.push({ introduced: stripV(from[1]), fixed: null });
33832
- continue;
33833
- }
33834
- if (/^all versions$/i.test(c)) {
33835
- out.push({ introduced: "0", fixed: null });
33836
- continue;
33837
- }
33838
- return null;
33839
- }
33840
- return out;
33841
- }
33842
33865
  function parseDisjunct(disjunct) {
33843
33866
  const first = disjunct[0];
33844
33867
  if (first === "(" || first === "[") return parseIntervalNotation(disjunct);
@@ -35101,7 +35124,7 @@ async function scanProject(setup, resolved, scanners) {
35101
35124
  durationMs: result.durationMs
35102
35125
  });
35103
35126
  if (result.status !== "ok") continue;
35104
- for (const finding of reconcileAgainstReported(result.findings, reportedByPackage)) {
35127
+ for (const finding of reconcileAgainstReported(result.findings, reportedByPackage, scanner.name).kept) {
35105
35128
  findings.push(finding);
35106
35129
  }
35107
35130
  }
@@ -35426,8 +35449,8 @@ function createUi(options) {
35426
35449
 
35427
35450
  // src/help.ts
35428
35451
  var CLI_VERSION = (function resolveVersion() {
35429
- if ("3.1.1".length > 0) {
35430
- return "3.1.1";
35452
+ if ("3.2.0".length > 0) {
35453
+ return "3.2.0";
35431
35454
  }
35432
35455
  const fromEnv = process.env.SENTINELLO_VERSION;
35433
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.1",
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": [