sentinello 3.7.3 → 3.7.4

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 +110 -31
  2. package/package.json +1 -1
package/dist/cli.cjs CHANGED
@@ -13885,7 +13885,7 @@ function asError(err) {
13885
13885
  }
13886
13886
 
13887
13887
  // ../../packages/core/src/advisory-rows.ts
13888
- var OSV_NORMALIZER_VERSION = 6;
13888
+ var OSV_NORMALIZER_VERSION = 7;
13889
13889
  var GEMNASIUM_NORMALIZER_VERSION = 7;
13890
13890
 
13891
13891
  // ../../packages/core/src/advisory-export.ts
@@ -34133,6 +34133,10 @@ function createGemnasiumScanner(deps) {
34133
34133
  durationMs: Date.now() - startedAt
34134
34134
  };
34135
34135
  }
34136
+ const notReady = evidenceNotReady(graph.packages, deps);
34137
+ if (notReady !== null) {
34138
+ return unauditable4("osv_db_not_seeded", "OSV withdrawal data for " + notReady + " is not complete yet", startedAt);
34139
+ }
34136
34140
  let findings;
34137
34141
  try {
34138
34142
  findings = matchPackages2(graph.packages, deps.lookup, deps.isEnabled);
@@ -34176,6 +34180,13 @@ function matchPackages2(packages, lookup, isEnabled) {
34176
34180
  }
34177
34181
  return findings;
34178
34182
  }
34183
+ function evidenceNotReady(packages, deps) {
34184
+ for (const ecosystem of groupByEcosystem2(packages).keys()) {
34185
+ if (!deps.isEnabled(ecosystem) || !comparatorForEcosystem(ecosystem)) continue;
34186
+ if (!deps.withdrawalEvidenceReady(ecosystem)) return ecosystem;
34187
+ }
34188
+ return null;
34189
+ }
34179
34190
  function toCanonicalAdvisory2(packageName, ecosystem, advisory) {
34180
34191
  return {
34181
34192
  id: advisory.advisoryId,
@@ -34191,7 +34202,7 @@ function toCanonicalAdvisory2(packageName, ecosystem, advisory) {
34191
34202
  severity: advisory.severity,
34192
34203
  summary: advisory.summary,
34193
34204
  url: advisory.url,
34194
- withdrawn: null
34205
+ withdrawn: advisory.withdrawn
34195
34206
  };
34196
34207
  }
34197
34208
  function groupByEcosystem2(packages) {
@@ -34219,6 +34230,37 @@ function unauditable4(reasonCode, message, startedAt) {
34219
34230
  };
34220
34231
  }
34221
34232
 
34233
+ // ../../packages/scanners/src/withdrawn.ts
34234
+ function stampWithdrawn(advisories, links) {
34235
+ return advisories.map(function stamp(advisory) {
34236
+ const withdrawn = withdrawnAt(advisory, links);
34237
+ return withdrawn === null ? advisory : { ...advisory, withdrawn };
34238
+ });
34239
+ }
34240
+ function stampWithdrawnByPackage(byPackage, linksByPackage) {
34241
+ const out = /* @__PURE__ */ new Map();
34242
+ for (const [name, advisories] of byPackage.entries()) {
34243
+ out.set(name, stampWithdrawn(advisories, linksByPackage.get(name) ?? []));
34244
+ }
34245
+ return out;
34246
+ }
34247
+ function withdrawnAt(advisory, links) {
34248
+ const identity = /* @__PURE__ */ new Set([advisory.advisoryId, ...advisory.aliases]);
34249
+ let latest = null;
34250
+ for (const link2 of links) {
34251
+ if (!sharesIdentifier(identity, link2)) continue;
34252
+ if (link2.withdrawn === null) return null;
34253
+ if (latest === null || link2.withdrawn > latest) latest = link2.withdrawn;
34254
+ }
34255
+ return latest;
34256
+ }
34257
+ function sharesIdentifier(identity, link2) {
34258
+ if (identity.has(link2.advisoryId)) return true;
34259
+ return link2.aliases.some(function inIdentity(alias) {
34260
+ return identity.has(alias);
34261
+ });
34262
+ }
34263
+
34222
34264
  // ../../packages/scanners/src/engine/reconcile.ts
34223
34265
  function findingIdentityKeys(finding) {
34224
34266
  const keys = [finding.advisoryId.toLowerCase()];
@@ -34504,7 +34546,7 @@ function normalizeOsvRecord(record2, ecosystem) {
34504
34546
  });
34505
34547
  }
34506
34548
  for (const row of byPackage.values()) {
34507
- if (row.ranges.length === 0 && row.versions.length === 0 && !malicious) continue;
34549
+ if (row.ranges.length === 0 && row.versions.length === 0 && !malicious && row.withdrawn === null) continue;
34508
34550
  rows.push(row);
34509
34551
  }
34510
34552
  return rows;
@@ -34670,10 +34712,7 @@ async function fetchOsvChangedIds(ecosystem, cursorMs, etag, options) {
34670
34712
  async function fetchOsvAdvisoryRows(id, ecosystem, options) {
34671
34713
  const record2 = await getJsonOrNull(osvAdvisoryUrl(id, ecosystem), options);
34672
34714
  if (record2 === null) return [];
34673
- const rows = normalizeOsvRecord(record2, ecosystem);
34674
- return rows.filter(function notWithdrawn(row) {
34675
- return row.withdrawn === null;
34676
- });
34715
+ return normalizeOsvRecord(record2, ecosystem);
34677
34716
  }
34678
34717
  function selectChangedIds(csv, cursorMs) {
34679
34718
  const ids = [];
@@ -48995,23 +49034,32 @@ function safeParse3(json3) {
48995
49034
  }
48996
49035
 
48997
49036
  // src/cache/lookup.ts
48998
- async function loadCacheForPackages(cacheDir, ecosystem, packageNames, sources) {
49037
+ async function loadCacheForPackages(cacheDir, ecosystem, packageNames, sources, meta3) {
48999
49038
  const wanted = new Set(packageNames);
49000
49039
  const osv = /* @__PURE__ */ new Map();
49001
49040
  const gemnasium = /* @__PURE__ */ new Map();
49041
+ const withGemnasium = sources.includes("gemnasium");
49042
+ const evidenceComplete = withGemnasium && isSeeded(meta3, "osv", ecosystem, OSV_NORMALIZER_VERSION);
49043
+ let osvRows = /* @__PURE__ */ new Map();
49044
+ if (sources.includes("osv") || evidenceComplete) {
49045
+ osvRows = await readRowsForPackages(advisoryFilePath(cacheDir, "osv", ecosystem), wanted);
49046
+ }
49002
49047
  if (sources.includes("osv")) {
49003
- const rows = await readRowsForPackages(advisoryFilePath(cacheDir, "osv", ecosystem), wanted);
49004
- for (const [name, list] of rows.entries()) {
49048
+ for (const [name, list] of osvRows.entries()) {
49005
49049
  osv.set(name, list.map(toOsvAdvisory));
49006
49050
  }
49007
49051
  }
49008
- if (sources.includes("gemnasium")) {
49052
+ if (withGemnasium) {
49009
49053
  const rows = await readRowsForPackages(advisoryFilePath(cacheDir, "gemnasium", ecosystem), wanted);
49010
49054
  for (const [name, list] of rows.entries()) {
49011
49055
  gemnasium.set(name, list.map(toGemnasiumAdvisory));
49012
49056
  }
49013
49057
  }
49014
- return { osv, gemnasium };
49058
+ return {
49059
+ osv,
49060
+ gemnasium: evidenceComplete ? stampWithdrawnByPackage(gemnasium, osvRows) : gemnasium,
49061
+ withdrawalsChecked: evidenceComplete
49062
+ };
49015
49063
  }
49016
49064
  function toOsvAdvisory(row) {
49017
49065
  return {
@@ -49034,7 +49082,8 @@ function toGemnasiumAdvisory(row) {
49034
49082
  versions: row.versions,
49035
49083
  severity: row.severity,
49036
49084
  summary: row.summary,
49037
- url: row.url
49085
+ url: row.url,
49086
+ withdrawn: null
49038
49087
  };
49039
49088
  }
49040
49089
  function registryFilePath(cacheDir) {
@@ -49151,17 +49200,26 @@ async function registryCacheSummary(cacheDir) {
49151
49200
  }
49152
49201
 
49153
49202
  // src/cache/sync.ts
49203
+ function syncTargets(sources) {
49204
+ const targets = sources.map(function forFindings(source) {
49205
+ return { source, purpose: "findings" };
49206
+ });
49207
+ if (sources.includes("gemnasium") && !sources.includes("osv")) {
49208
+ targets.push({ source: "osv", purpose: "withdrawal-evidence" });
49209
+ }
49210
+ return targets;
49211
+ }
49154
49212
  async function planSync(options) {
49155
49213
  const meta3 = await readCacheMeta(options.cacheDir);
49156
49214
  const items = [];
49157
49215
  let seedBytes = 0;
49158
- for (const source of options.sources) {
49216
+ for (const { source, purpose } of syncTargets(options.sources)) {
49159
49217
  if (source === "osv" && osvFeedDisabled()) continue;
49160
49218
  if (source === "gemnasium" && gemnasiumFeedDisabled()) continue;
49161
49219
  const normalizerVersion = source === "osv" ? OSV_NORMALIZER_VERSION : GEMNASIUM_NORMALIZER_VERSION;
49162
49220
  const seeded = isSeeded(meta3, source, options.ecosystem, normalizerVersion);
49163
49221
  if (seeded) {
49164
- items.push({ source, ecosystem: options.ecosystem, kind: "refresh", downloadBytes: null, downloadBytesEstimated: false });
49222
+ items.push({ source, ecosystem: options.ecosystem, purpose, kind: "refresh", downloadBytes: null, downloadBytesEstimated: false });
49165
49223
  continue;
49166
49224
  }
49167
49225
  const estimate = await estimateSeedBytes(source, options.ecosystem, options.abortSignal);
@@ -49169,6 +49227,7 @@ async function planSync(options) {
49169
49227
  items.push({
49170
49228
  source,
49171
49229
  ecosystem: options.ecosystem,
49230
+ purpose,
49172
49231
  kind: "seed",
49173
49232
  downloadBytes: estimate.bytes,
49174
49233
  downloadBytesEstimated: estimate.estimated
@@ -49307,32 +49366,38 @@ async function refreshOsv(options, meta3, item) {
49307
49366
  return { source: "osv", ecosystem: item.ecosystem, status: "unchanged", rowCount: state?.recordCount ?? 0, message: null };
49308
49367
  }
49309
49368
  const append = [];
49310
- const replaced = /* @__PURE__ */ new Set();
49311
- let skipped = 0;
49369
+ let failure = null;
49312
49370
  for (const id of changed.ids) {
49313
49371
  if (options.abortSignal && options.abortSignal.aborted) {
49314
- skipped += 1;
49372
+ failure = "the refresh was cancelled before every changed advisory was re-read";
49315
49373
  break;
49316
49374
  }
49317
49375
  try {
49318
49376
  const rows = await fetchOsvAdvisoryRows(id, item.ecosystem, fetchOptionsFor(options, item));
49319
49377
  for (const row of rows) append.push(row);
49320
- replaced.add(id);
49321
- } catch {
49322
- skipped += 1;
49323
- continue;
49378
+ } catch (err) {
49379
+ failure = id + " could not be re-read (" + errText(err) + ")";
49380
+ break;
49324
49381
  }
49325
49382
  }
49383
+ if (failure !== null) {
49384
+ if (state) state.etag = null;
49385
+ return {
49386
+ source: "osv",
49387
+ ecosystem: item.ecosystem,
49388
+ status: "error",
49389
+ rowCount: state?.recordCount ?? 0,
49390
+ message: failure + "; the previous OSV data is kept until a refresh re-reads all " + changed.ids.length + " changed advisories"
49391
+ };
49392
+ }
49326
49393
  const path = advisoryFilePath(options.cacheDir, "osv", item.ecosystem);
49327
- const rowCount = await rewriteRows(path, { dropAdvisoryIds: replaced, append });
49328
- const complete = skipped === 0;
49329
- const previousCursor = state?.cursorIso ?? null;
49394
+ const rowCount = await rewriteRows(path, { dropAdvisoryIds: new Set(changed.ids), append });
49330
49395
  setSourceState(meta3, "osv", item.ecosystem, {
49331
49396
  normalizerVersion: OSV_NORMALIZER_VERSION,
49332
49397
  recordCount: rowCount,
49333
49398
  refreshedAt: Date.now(),
49334
- cursorIso: complete ? changed.newestIso ?? previousCursor : previousCursor,
49335
- etag: complete ? changed.etag : null
49399
+ cursorIso: changed.newestIso ?? state?.cursorIso ?? null,
49400
+ etag: changed.etag
49336
49401
  });
49337
49402
  return {
49338
49403
  source: "osv",
@@ -49893,6 +49958,12 @@ function buildScanners(setup, cache) {
49893
49958
  },
49894
49959
  isEnabled: function isEnabled(ecosystem) {
49895
49960
  return ecosystem === setup.ecosystem;
49961
+ },
49962
+ // Always ready: the cache was stamped (or not) before the scan, and a one-shot report with no
49963
+ // complete OSV evidence reports gemnasium unstamped under a warning instead of refusing it.
49964
+ // Only the portal, whose findings persist across scans, has to wait for the evidence.
49965
+ withdrawalEvidenceReady: function withdrawalEvidenceReady() {
49966
+ return true;
49896
49967
  }
49897
49968
  }));
49898
49969
  }
@@ -50100,6 +50171,9 @@ function createUi(options) {
50100
50171
  write("");
50101
50172
  for (const item of seeds) {
50102
50173
  write(" " + sourceLabel(item.source) + c.dim + " " + formatBytes(item.downloadBytes, item.downloadBytesEstimated) + c.reset);
50174
+ if (item.purpose === "withdrawal-evidence") {
50175
+ write(" " + c.dim + "used only to drop advisories GitHub has withdrawn" + c.reset);
50176
+ }
50103
50177
  }
50104
50178
  write("");
50105
50179
  write(" " + c.dim + "Downloaded once from the upstream feeds, then kept fresh incrementally \u2014" + c.reset);
@@ -50208,6 +50282,9 @@ function createUi(options) {
50208
50282
  write(" " + c.green + "\u2713" + c.reset + " " + sourceLabel(outcome.source) + c.dim + " " + verb + " (" + outcome.rowCount.toLocaleString() + " advisories)" + c.reset);
50209
50283
  }
50210
50284
  }
50285
+ function withdrawalsUnchecked() {
50286
+ write(" " + c.yellow + "!" + c.reset + c.dim + " OSV withdrawal data is not downloaded \u2014 GitLab gemnasium may list advisories GitHub has withdrawn" + c.reset);
50287
+ }
50211
50288
  function sourceLabel(source) {
50212
50289
  if (source === "osv") return "OSV";
50213
50290
  if (source === "gemnasium") return "GitLab gemnasium";
@@ -50366,6 +50443,7 @@ function createUi(options) {
50366
50443
  syncProgress,
50367
50444
  syncRetry,
50368
50445
  syncDone,
50446
+ withdrawalsUnchecked,
50369
50447
  scanStart,
50370
50448
  scanProject: scanProject2,
50371
50449
  scanProjectDone,
@@ -50377,8 +50455,8 @@ function createUi(options) {
50377
50455
 
50378
50456
  // src/help.ts
50379
50457
  var CLI_VERSION = (function resolveVersion() {
50380
- if ("3.7.3".length > 0) {
50381
- return "3.7.3";
50458
+ if ("3.7.4".length > 0) {
50459
+ return "3.7.4";
50382
50460
  }
50383
50461
  const fromEnv = process.env.SENTINELLO_VERSION;
50384
50462
  if (fromEnv && fromEnv.trim().length > 0) return fromEnv.trim();
@@ -50642,10 +50720,11 @@ async function runScan(options, cacheDir, ui) {
50642
50720
  } else if (options.offline) {
50643
50721
  ui.offlineNotice(options.includeNpmAudit);
50644
50722
  }
50723
+ const meta3 = await readCacheMeta(cacheDir);
50645
50724
  const resolved2 = await resolveProjects(projects);
50646
50725
  const packageNames = collectPackageNames(resolved2);
50647
- const cache = await loadCacheForPackages(cacheDir, DEFAULT_ECOSYSTEM, packageNames, sources);
50648
- const meta3 = await readCacheMeta(cacheDir);
50726
+ const cache = await loadCacheForPackages(cacheDir, DEFAULT_ECOSYSTEM, packageNames, sources, meta3);
50727
+ if (sources.includes("gemnasium") && !cache.withdrawalsChecked) ui.withdrawalsUnchecked();
50649
50728
  const registryStore = options.offline ? null : await loadRegistryStore(cacheDir);
50650
50729
  const setup = {
50651
50730
  cacheDir,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sentinello",
3
- "version": "3.7.3",
3
+ "version": "3.7.4",
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": [