sentinello 3.0.0 → 3.0.1

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 +7 -3
  2. package/dist/cli.cjs +561 -385
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -76,12 +76,16 @@ Run `sentinello --help` for the full list. The ones people reach for:
76
76
  | `--fail-on <level>` | exit 2 when matched, for CI |
77
77
  | `--out <file\|->` | where the advisory goes |
78
78
  | `--json` | machine-readable output |
79
- | `-y, --yes` | accept the first-run download without asking |
79
+ | `-y, --yes` | accept the first-run download without asking. Required on a non-TTY |
80
80
  | `--offline` | use the cache as-is, no network |
81
81
  | `--doctor` | cache status, resolved settings, and what was skipped and why |
82
82
 
83
- Exit codes: `0` completed, `1` a scan or configuration error, `2` the `--fail-on` threshold was met.
84
- Findings alone are not a failure.
83
+ Exit codes: `0` completed, `1` a scan or configuration error — including a `--fail-on` run that could
84
+ not consult one of its sources, `2` the `--fail-on` threshold was met. Findings alone are not a failure.
85
+
86
+ In CI, pass both: `npx --yes sentinello --yes --fail-on high`. The first `--yes` answers npm's own
87
+ "Ok to proceed?" prompt for an uncached package; the second accepts the first-run advisory download,
88
+ which is refused on a non-interactive terminal.
85
89
 
86
90
  ## Requires
87
91
 
package/dist/cli.cjs CHANGED
@@ -6,6 +6,8 @@ var path = require('path');
6
6
  var child_process = require('child_process');
7
7
  var fs = require('fs');
8
8
  var crypto = require('crypto');
9
+ var http = require('http');
10
+ var https = require('https');
9
11
  var os = require('os');
10
12
  var zlib = require('zlib');
11
13
  var promises$2 = require('stream/promises');
@@ -3725,9 +3727,9 @@ var require_stream_duplex = __commonJS({
3725
3727
  module2.exports = Duplex;
3726
3728
  var util = Object.create(require_util());
3727
3729
  util.inherits = require_inherits();
3728
- var Readable2 = require_stream_readable();
3730
+ var Readable = require_stream_readable();
3729
3731
  var Writable = require_stream_writable();
3730
- util.inherits(Duplex, Readable2);
3732
+ util.inherits(Duplex, Readable);
3731
3733
  {
3732
3734
  keys = objectKeys(Writable.prototype);
3733
3735
  for (v = 0; v < keys.length; v++) {
@@ -3740,7 +3742,7 @@ var require_stream_duplex = __commonJS({
3740
3742
  var v;
3741
3743
  function Duplex(options) {
3742
3744
  if (!(this instanceof Duplex)) return new Duplex(options);
3743
- Readable2.call(this, options);
3745
+ Readable.call(this, options);
3744
3746
  Writable.call(this, options);
3745
3747
  if (options && options.readable === false) this.readable = false;
3746
3748
  if (options && options.writable === false) this.writable = false;
@@ -4028,10 +4030,10 @@ var require_string_decoder = __commonJS({
4028
4030
  var require_stream_readable = __commonJS({
4029
4031
  "../../node_modules/.pnpm/readable-stream@2.3.8/node_modules/readable-stream/lib/_stream_readable.js"(exports2, module2) {
4030
4032
  var pna = require_process_nextick_args();
4031
- module2.exports = Readable2;
4033
+ module2.exports = Readable;
4032
4034
  var isArray = require_isarray();
4033
4035
  var Duplex;
4034
- Readable2.ReadableState = ReadableState;
4036
+ Readable.ReadableState = ReadableState;
4035
4037
  __require("events").EventEmitter;
4036
4038
  var EElistenerCount = function(emitter, type2) {
4037
4039
  return emitter.listeners(type2).length;
@@ -4059,7 +4061,7 @@ var require_stream_readable = __commonJS({
4059
4061
  var BufferList = require_BufferList();
4060
4062
  var destroyImpl = require_destroy();
4061
4063
  var StringDecoder;
4062
- util.inherits(Readable2, Stream);
4064
+ util.inherits(Readable, Stream);
4063
4065
  var kProxyEvents = ["error", "close", "destroy", "pause", "resume"];
4064
4066
  function prependListener(emitter, event, fn) {
4065
4067
  if (typeof emitter.prependListener === "function") return emitter.prependListener(event, fn);
@@ -4105,9 +4107,9 @@ var require_stream_readable = __commonJS({
4105
4107
  this.encoding = options.encoding;
4106
4108
  }
4107
4109
  }
4108
- function Readable2(options) {
4110
+ function Readable(options) {
4109
4111
  Duplex = Duplex || require_stream_duplex();
4110
- if (!(this instanceof Readable2)) return new Readable2(options);
4112
+ if (!(this instanceof Readable)) return new Readable(options);
4111
4113
  this._readableState = new ReadableState(options, this);
4112
4114
  this.readable = true;
4113
4115
  if (options) {
@@ -4116,7 +4118,7 @@ var require_stream_readable = __commonJS({
4116
4118
  }
4117
4119
  Stream.call(this);
4118
4120
  }
4119
- Object.defineProperty(Readable2.prototype, "destroyed", {
4121
+ Object.defineProperty(Readable.prototype, "destroyed", {
4120
4122
  get: function() {
4121
4123
  if (this._readableState === void 0) {
4122
4124
  return false;
@@ -4130,13 +4132,13 @@ var require_stream_readable = __commonJS({
4130
4132
  this._readableState.destroyed = value;
4131
4133
  }
4132
4134
  });
4133
- Readable2.prototype.destroy = destroyImpl.destroy;
4134
- Readable2.prototype._undestroy = destroyImpl.undestroy;
4135
- Readable2.prototype._destroy = function(err, cb) {
4135
+ Readable.prototype.destroy = destroyImpl.destroy;
4136
+ Readable.prototype._undestroy = destroyImpl.undestroy;
4137
+ Readable.prototype._destroy = function(err, cb) {
4136
4138
  this.push(null);
4137
4139
  cb(err);
4138
4140
  };
4139
- Readable2.prototype.push = function(chunk, encoding) {
4141
+ Readable.prototype.push = function(chunk, encoding) {
4140
4142
  var state = this._readableState;
4141
4143
  var skipChunkCheck;
4142
4144
  if (!state.objectMode) {
@@ -4153,7 +4155,7 @@ var require_stream_readable = __commonJS({
4153
4155
  }
4154
4156
  return readableAddChunk(this, chunk, encoding, false, skipChunkCheck);
4155
4157
  };
4156
- Readable2.prototype.unshift = function(chunk) {
4158
+ Readable.prototype.unshift = function(chunk) {
4157
4159
  return readableAddChunk(this, chunk, null, true, false);
4158
4160
  };
4159
4161
  function readableAddChunk(stream, chunk, encoding, addToFront, skipChunkCheck) {
@@ -4213,10 +4215,10 @@ var require_stream_readable = __commonJS({
4213
4215
  function needMoreData(state) {
4214
4216
  return !state.ended && (state.needReadable || state.length < state.highWaterMark || state.length === 0);
4215
4217
  }
4216
- Readable2.prototype.isPaused = function() {
4218
+ Readable.prototype.isPaused = function() {
4217
4219
  return this._readableState.flowing === false;
4218
4220
  };
4219
- Readable2.prototype.setEncoding = function(enc) {
4221
+ Readable.prototype.setEncoding = function(enc) {
4220
4222
  if (!StringDecoder) StringDecoder = require_string_decoder().StringDecoder;
4221
4223
  this._readableState.decoder = new StringDecoder(enc);
4222
4224
  this._readableState.encoding = enc;
@@ -4252,7 +4254,7 @@ var require_stream_readable = __commonJS({
4252
4254
  }
4253
4255
  return state.length;
4254
4256
  }
4255
- Readable2.prototype.read = function(n) {
4257
+ Readable.prototype.read = function(n) {
4256
4258
  debug("read", n);
4257
4259
  n = parseInt(n, 10);
4258
4260
  var state = this._readableState;
@@ -4347,10 +4349,10 @@ var require_stream_readable = __commonJS({
4347
4349
  }
4348
4350
  state.readingMore = false;
4349
4351
  }
4350
- Readable2.prototype._read = function(n) {
4352
+ Readable.prototype._read = function(n) {
4351
4353
  this.emit("error", new Error("_read() is not implemented"));
4352
4354
  };
4353
- Readable2.prototype.pipe = function(dest, pipeOpts) {
4355
+ Readable.prototype.pipe = function(dest, pipeOpts) {
4354
4356
  var src = this;
4355
4357
  var state = this._readableState;
4356
4358
  switch (state.pipesCount) {
@@ -4455,7 +4457,7 @@ var require_stream_readable = __commonJS({
4455
4457
  }
4456
4458
  };
4457
4459
  }
4458
- Readable2.prototype.unpipe = function(dest) {
4460
+ Readable.prototype.unpipe = function(dest) {
4459
4461
  var state = this._readableState;
4460
4462
  var unpipeInfo = { hasUnpiped: false };
4461
4463
  if (state.pipesCount === 0) return this;
@@ -4487,7 +4489,7 @@ var require_stream_readable = __commonJS({
4487
4489
  dest.emit("unpipe", this, unpipeInfo);
4488
4490
  return this;
4489
4491
  };
4490
- Readable2.prototype.on = function(ev, fn) {
4492
+ Readable.prototype.on = function(ev, fn) {
4491
4493
  var res = Stream.prototype.on.call(this, ev, fn);
4492
4494
  if (ev === "data") {
4493
4495
  if (this._readableState.flowing !== false) this.resume();
@@ -4505,12 +4507,12 @@ var require_stream_readable = __commonJS({
4505
4507
  }
4506
4508
  return res;
4507
4509
  };
4508
- Readable2.prototype.addListener = Readable2.prototype.on;
4510
+ Readable.prototype.addListener = Readable.prototype.on;
4509
4511
  function nReadingNextTick(self2) {
4510
4512
  debug("readable nexttick read 0");
4511
4513
  self2.read(0);
4512
4514
  }
4513
- Readable2.prototype.resume = function() {
4515
+ Readable.prototype.resume = function() {
4514
4516
  var state = this._readableState;
4515
4517
  if (!state.flowing) {
4516
4518
  debug("resume");
@@ -4536,7 +4538,7 @@ var require_stream_readable = __commonJS({
4536
4538
  flow(stream);
4537
4539
  if (state.flowing && !state.reading) stream.read(0);
4538
4540
  }
4539
- Readable2.prototype.pause = function() {
4541
+ Readable.prototype.pause = function() {
4540
4542
  debug("call pause flowing=%j", this._readableState.flowing);
4541
4543
  if (false !== this._readableState.flowing) {
4542
4544
  debug("pause");
@@ -4551,7 +4553,7 @@ var require_stream_readable = __commonJS({
4551
4553
  while (state.flowing && stream.read() !== null) {
4552
4554
  }
4553
4555
  }
4554
- Readable2.prototype.wrap = function(stream) {
4556
+ Readable.prototype.wrap = function(stream) {
4555
4557
  var _this = this;
4556
4558
  var state = this._readableState;
4557
4559
  var paused = false;
@@ -4595,7 +4597,7 @@ var require_stream_readable = __commonJS({
4595
4597
  };
4596
4598
  return this;
4597
4599
  };
4598
- Object.defineProperty(Readable2.prototype, "readableHighWaterMark", {
4600
+ Object.defineProperty(Readable.prototype, "readableHighWaterMark", {
4599
4601
  // making it explicit this property is not enumerable
4600
4602
  // because otherwise some prototype manipulation in
4601
4603
  // userland will fail
@@ -4604,7 +4606,7 @@ var require_stream_readable = __commonJS({
4604
4606
  return this._readableState.highWaterMark;
4605
4607
  }
4606
4608
  });
4607
- Readable2._fromList = fromList;
4609
+ Readable._fromList = fromList;
4608
4610
  function fromList(n, state) {
4609
4611
  if (state.length === 0) return null;
4610
4612
  var ret2;
@@ -13117,6 +13119,15 @@ function compareSeverity(a, b) {
13117
13119
  function meetsSeverityFloor(severity, floor) {
13118
13120
  return severityWeight(severity) >= severityWeight(floor);
13119
13121
  }
13122
+ var SOURCE_UNAVAILABLE_REASON_CODES = [
13123
+ "osv_db_not_seeded",
13124
+ "osv_db_unavailable",
13125
+ "gemnasium_db_not_seeded",
13126
+ "gemnasium_db_unavailable"
13127
+ ];
13128
+ function isSourceUnavailableReason(code) {
13129
+ return SOURCE_UNAVAILABLE_REASON_CODES.includes(code);
13130
+ }
13120
13131
 
13121
13132
  // ../../packages/core/src/advisory-rows.ts
13122
13133
  var OSV_NORMALIZER_VERSION = 3;
@@ -33206,288 +33217,6 @@ function reconcileAgainstReported(findings, reportedByPackage) {
33206
33217
  // ../../packages/scanners/src/index.ts
33207
33218
  var registry2 = /* @__PURE__ */ new Map();
33208
33219
  registry2.set(npmAuditPlugin.name, npmAuditPlugin);
33209
- var CACHE_SCHEMA_VERSION = 1;
33210
- function emptyMeta() {
33211
- return { schemaVersion: CACHE_SCHEMA_VERSION, sources: { osv: {}, gemnasium: {} } };
33212
- }
33213
- function resolveCacheDir(override) {
33214
- if (override && override.trim().length > 0) return path.resolve(override.trim());
33215
- const fromEnv = process.env.SENTINELLO_CACHE_DIR;
33216
- if (fromEnv && fromEnv.trim().length > 0) return path.resolve(fromEnv.trim());
33217
- const xdg = process.env.XDG_CACHE_HOME;
33218
- if (xdg && xdg.trim().length > 0) return path.join(path.resolve(xdg.trim()), "sentinello");
33219
- return path.join(os.homedir(), ".cache", "sentinello");
33220
- }
33221
- function advisoryFilePath(cacheDir, source, ecosystem) {
33222
- const slug = ecosystem.replace(/[^a-zA-Z0-9._-]+/g, "-");
33223
- return path.join(cacheDir, source + "-" + slug + ".ndjson.gz");
33224
- }
33225
- function metaPath(cacheDir) {
33226
- return path.join(cacheDir, "meta.json");
33227
- }
33228
- async function ensureCacheDir(cacheDir) {
33229
- await promises.mkdir(cacheDir, { recursive: true });
33230
- }
33231
- async function readCacheMeta(cacheDir) {
33232
- let text;
33233
- try {
33234
- text = await promises.readFile(metaPath(cacheDir), "utf8");
33235
- } catch {
33236
- return emptyMeta();
33237
- }
33238
- let parsed;
33239
- try {
33240
- parsed = JSON.parse(text);
33241
- } catch {
33242
- return emptyMeta();
33243
- }
33244
- if (!parsed || typeof parsed !== "object") return emptyMeta();
33245
- const meta3 = parsed;
33246
- if (meta3.schemaVersion !== CACHE_SCHEMA_VERSION) return emptyMeta();
33247
- const sources = meta3.sources;
33248
- if (!sources || typeof sources !== "object") return emptyMeta();
33249
- return {
33250
- schemaVersion: CACHE_SCHEMA_VERSION,
33251
- sources: {
33252
- osv: sources.osv ?? {},
33253
- gemnasium: sources.gemnasium ?? {}
33254
- }
33255
- };
33256
- }
33257
- async function writeCacheMeta(cacheDir, meta3) {
33258
- await ensureCacheDir(cacheDir);
33259
- const target = metaPath(cacheDir);
33260
- const tmp = target + ".tmp";
33261
- await promises.writeFile(tmp, JSON.stringify(meta3, null, 4) + "\n", "utf8");
33262
- await promises.rename(tmp, target);
33263
- }
33264
- function getSourceState(meta3, source, ecosystem) {
33265
- return meta3.sources[source][ecosystem] ?? null;
33266
- }
33267
- function setSourceState(meta3, source, ecosystem, state) {
33268
- meta3.sources[source][ecosystem] = state;
33269
- }
33270
- function isSeeded(meta3, source, ecosystem, normalizerVersion) {
33271
- const state = getSourceState(meta3, source, ecosystem);
33272
- if (!state) return false;
33273
- return state.normalizerVersion === normalizerVersion && state.recordCount > 0;
33274
- }
33275
- async function tryAcquireLock(cacheDir) {
33276
- await ensureCacheDir(cacheDir);
33277
- const path$1 = path.join(cacheDir, ".lock");
33278
- try {
33279
- await promises.writeFile(path$1, String(process.pid), { flag: "wx" });
33280
- } catch {
33281
- if (await isStaleLock(path$1)) {
33282
- await promises.rm(path$1, { force: true });
33283
- return await tryAcquireLock(cacheDir);
33284
- }
33285
- return null;
33286
- }
33287
- async function release() {
33288
- await promises.rm(path$1, { force: true });
33289
- }
33290
- return { release };
33291
- }
33292
- var LOCK_STALE_MS = 30 * 60 * 1e3;
33293
- async function isStaleLock(path) {
33294
- try {
33295
- const info = await promises.stat(path);
33296
- return Date.now() - info.mtimeMs > LOCK_STALE_MS;
33297
- } catch {
33298
- return false;
33299
- }
33300
- }
33301
- var FIELD_SEPARATOR = " ";
33302
- var LINE_SEPARATOR = "\n";
33303
- function encodeRow(row) {
33304
- const name = row.packageName.replace(/[\t\n\r]/g, "");
33305
- const id = row.advisoryId.replace(/[\t\n\r]/g, "");
33306
- return name + FIELD_SEPARATOR + id + FIELD_SEPARATOR + JSON.stringify(row);
33307
- }
33308
- function createRowWriter(path) {
33309
- const tmpPath = path + ".tmp";
33310
- const source = new stream.PassThrough();
33311
- const gzip = zlib.createGzip();
33312
- const done = promises$2.pipeline(source, gzip, fs.createWriteStream(tmpPath));
33313
- let rowCount = 0;
33314
- let failed = null;
33315
- done.catch(function capture(err) {
33316
- failed = err instanceof Error ? err : new Error(String(err));
33317
- });
33318
- async function push(chunk, added) {
33319
- if (failed) throw failed;
33320
- rowCount += added;
33321
- if (!source.write(chunk)) {
33322
- await new Promise(function waitForDrain(resolve5) {
33323
- source.once("drain", resolve5);
33324
- });
33325
- }
33326
- }
33327
- async function write(rows) {
33328
- if (rows.length === 0) return;
33329
- await push(rows.map(encodeRow).join(LINE_SEPARATOR) + LINE_SEPARATOR, rows.length);
33330
- }
33331
- async function writeRaw(lines) {
33332
- if (lines.length === 0) return;
33333
- await push(lines.join(LINE_SEPARATOR) + LINE_SEPARATOR, lines.length);
33334
- }
33335
- async function commit() {
33336
- source.end();
33337
- await done;
33338
- await promises.rename(tmpPath, path);
33339
- return rowCount;
33340
- }
33341
- async function abort() {
33342
- source.destroy();
33343
- try {
33344
- await done;
33345
- } catch {
33346
- }
33347
- await promises.rm(tmpPath, { force: true });
33348
- }
33349
- return { write, writeRaw, commit, abort };
33350
- }
33351
- async function readRowsForPackages(path, packageNames) {
33352
- const out = /* @__PURE__ */ new Map();
33353
- if (packageNames.size === 0) return out;
33354
- let raw;
33355
- try {
33356
- raw = await promises.readFile(path);
33357
- } catch {
33358
- return out;
33359
- }
33360
- const text = zlib.gunzipSync(raw).toString("utf8");
33361
- let offset = 0;
33362
- while (offset < text.length) {
33363
- let end = text.indexOf(LINE_SEPARATOR, offset);
33364
- if (end === -1) end = text.length;
33365
- const nameEnd = text.indexOf(FIELD_SEPARATOR, offset);
33366
- if (nameEnd === -1 || nameEnd > end) {
33367
- offset = end + 1;
33368
- continue;
33369
- }
33370
- const name = text.slice(offset, nameEnd);
33371
- if (packageNames.has(name)) {
33372
- const idEnd = text.indexOf(FIELD_SEPARATOR, nameEnd + 1);
33373
- if (idEnd !== -1 && idEnd < end) {
33374
- const parsed = safeParse3(text.slice(idEnd + 1, end));
33375
- if (parsed) {
33376
- const list = out.get(name);
33377
- if (list) {
33378
- list.push(parsed);
33379
- } else {
33380
- out.set(name, [parsed]);
33381
- }
33382
- }
33383
- }
33384
- }
33385
- offset = end + 1;
33386
- }
33387
- return out;
33388
- }
33389
- async function rewriteRows(path, input) {
33390
- let surviving = [];
33391
- try {
33392
- const raw = await promises.readFile(path);
33393
- const text = zlib.gunzipSync(raw).toString("utf8");
33394
- for (const line of text.split(LINE_SEPARATOR)) {
33395
- if (line.length === 0) continue;
33396
- const nameEnd = line.indexOf(FIELD_SEPARATOR);
33397
- if (nameEnd === -1) continue;
33398
- const idEnd = line.indexOf(FIELD_SEPARATOR, nameEnd + 1);
33399
- if (idEnd === -1) continue;
33400
- const advisoryId = line.slice(nameEnd + 1, idEnd);
33401
- if (input.dropAdvisoryIds.has(advisoryId)) continue;
33402
- surviving.push(line);
33403
- }
33404
- } catch {
33405
- surviving = [];
33406
- }
33407
- const writer = createRowWriter(path);
33408
- try {
33409
- const CHUNK = 5e3;
33410
- for (let i = 0; i < surviving.length; i += CHUNK) {
33411
- await writer.writeRaw(surviving.slice(i, i + CHUNK));
33412
- }
33413
- await writer.write(input.append);
33414
- return await writer.commit();
33415
- } catch (err) {
33416
- await writer.abort();
33417
- throw err;
33418
- }
33419
- }
33420
- async function countRows(path) {
33421
- try {
33422
- const raw = await promises.readFile(path);
33423
- const text = zlib.gunzipSync(raw).toString("utf8");
33424
- let count = 0;
33425
- let offset = 0;
33426
- for (; ; ) {
33427
- const next = text.indexOf(LINE_SEPARATOR, offset);
33428
- if (next === -1) break;
33429
- count++;
33430
- offset = next + 1;
33431
- }
33432
- return count;
33433
- } catch {
33434
- return 0;
33435
- }
33436
- }
33437
- function safeParse3(json3) {
33438
- try {
33439
- return JSON.parse(json3);
33440
- } catch {
33441
- return null;
33442
- }
33443
- }
33444
-
33445
- // src/cache/lookup.ts
33446
- async function loadCacheForPackages(cacheDir, ecosystem, packageNames, sources) {
33447
- const wanted = new Set(packageNames);
33448
- const osv = /* @__PURE__ */ new Map();
33449
- const gemnasium = /* @__PURE__ */ new Map();
33450
- if (sources.includes("osv")) {
33451
- const rows = await readRowsForPackages(advisoryFilePath(cacheDir, "osv", ecosystem), wanted);
33452
- for (const [name, list] of rows.entries()) {
33453
- osv.set(name, list.map(toOsvAdvisory));
33454
- }
33455
- }
33456
- if (sources.includes("gemnasium")) {
33457
- const rows = await readRowsForPackages(advisoryFilePath(cacheDir, "gemnasium", ecosystem), wanted);
33458
- for (const [name, list] of rows.entries()) {
33459
- gemnasium.set(name, list.map(toGemnasiumAdvisory));
33460
- }
33461
- }
33462
- return { osv, gemnasium };
33463
- }
33464
- function cacheEcosystemKey(ecosystem) {
33465
- const def = getEcosystem(ecosystem);
33466
- return def ? def.osvEcosystem : ecosystem;
33467
- }
33468
- function toOsvAdvisory(row) {
33469
- return {
33470
- advisoryId: row.advisoryId,
33471
- aliases: row.aliases,
33472
- ranges: row.ranges,
33473
- versions: row.versions,
33474
- severity: row.severity,
33475
- summary: row.summary,
33476
- url: row.url,
33477
- malicious: row.malicious
33478
- };
33479
- }
33480
- function toGemnasiumAdvisory(row) {
33481
- return {
33482
- advisoryId: row.advisoryId,
33483
- aliases: row.aliases,
33484
- ranges: row.ranges,
33485
- versions: row.versions,
33486
- severity: row.severity,
33487
- summary: row.summary,
33488
- url: row.url
33489
- };
33490
- }
33491
33220
 
33492
33221
  // ../../packages/feeds/src/osv/feed.ts
33493
33222
  var import_unzipper = __toESM(require_unzip2());
@@ -33521,8 +33250,8 @@ function isSlowTransientStatus(status) {
33521
33250
  var FAST_MAX_ATTEMPTS = 4;
33522
33251
  var RETRY_BASE_MS = 1e3;
33523
33252
  var RETRY_FACTOR = 3;
33524
- var DEFAULT_RETRY_WAIT_MS = 18e4;
33525
- var SLOW_RETRY_WAITS_MS = [45e3, 6e4, 75e3];
33253
+ var DEFAULT_RETRY_WAIT_MS = 15e3;
33254
+ var SLOW_RETRY_WAITS_MS = [2e3, 5e3, 8e3];
33526
33255
  function delay(ms) {
33527
33256
  return new Promise(function schedule(resolve5) {
33528
33257
  setTimeout(resolve5, ms);
@@ -33532,7 +33261,7 @@ function retryBudgetMs(options) {
33532
33261
  if (options && typeof options.retryWaitMs === "number" && options.retryWaitMs >= 0) return options.retryWaitMs;
33533
33262
  return DEFAULT_RETRY_WAIT_MS;
33534
33263
  }
33535
- async function fetchWithRetry(url2, init, options, timeoutMs) {
33264
+ async function withRetry(describe3, perform, discard, options, timeoutMs) {
33536
33265
  const budgetMs = retryBudgetMs(options);
33537
33266
  const startedAt = Date.now();
33538
33267
  let lastStatus;
@@ -33541,19 +33270,19 @@ async function fetchWithRetry(url2, init, options, timeoutMs) {
33541
33270
  let slowAttempts = 0;
33542
33271
  for (; ; ) {
33543
33272
  attempt++;
33544
- const response = await fetch(url2, { ...init, signal: signalFor(options, timeoutMs) });
33273
+ const response = await perform(signalFor(options, timeoutMs));
33545
33274
  const slow = isSlowTransientStatus(response.status);
33546
33275
  if (!slow && !isFastTransientStatus(response.status)) return response;
33547
33276
  lastStatus = response.status;
33548
- if (response.body) await response.body.cancel();
33277
+ await discard(response);
33549
33278
  if (options && options.abortSignal && options.abortSignal.aborted) break;
33550
33279
  if (slow) {
33551
- const elapsedMs = Date.now() - startedAt;
33552
- if (elapsedMs >= budgetMs) break;
33280
+ const elapsedMs2 = Date.now() - startedAt;
33281
+ if (elapsedMs2 >= budgetMs) break;
33553
33282
  const nominalMs = SLOW_RETRY_WAITS_MS[Math.min(slowAttempts, SLOW_RETRY_WAITS_MS.length - 1)];
33554
- const waitMs = Math.min(nominalMs, budgetMs - elapsedMs);
33283
+ const waitMs = Math.min(nominalMs, budgetMs - elapsedMs2);
33555
33284
  slowAttempts++;
33556
- if (options && options.onRetry) options.onRetry({ status: lastStatus, attempt, waitMs, elapsedMs, budgetMs });
33285
+ if (options && options.onRetry) options.onRetry({ status: lastStatus, attempt, waitMs, elapsedMs: elapsedMs2, budgetMs });
33557
33286
  await delay(waitMs);
33558
33287
  continue;
33559
33288
  }
@@ -33561,11 +33290,50 @@ async function fetchWithRetry(url2, init, options, timeoutMs) {
33561
33290
  if (fastAttempts >= FAST_MAX_ATTEMPTS) break;
33562
33291
  await delay(RETRY_BASE_MS * Math.pow(RETRY_FACTOR, fastAttempts - 1));
33563
33292
  }
33564
- throw new Error(describeMethod(init) + " " + url2 + " failed: HTTP " + lastStatus + " after " + attempt + " attempts");
33293
+ const elapsedMs = Date.now() - startedAt;
33294
+ throw new Error(
33295
+ describe3 + " failed: HTTP " + lastStatus + " after " + attempt + " attempts over " + Math.round(elapsedMs / 1e3) + "s"
33296
+ );
33297
+ }
33298
+ async function fetchWithRetry(url2, init, options, timeoutMs) {
33299
+ return await withRetry(
33300
+ describeMethod(init) + " " + url2,
33301
+ function perform(signal) {
33302
+ return fetch(url2, { ...init, signal });
33303
+ },
33304
+ async function discard(response) {
33305
+ if (response.body) await response.body.cancel();
33306
+ },
33307
+ options,
33308
+ timeoutMs
33309
+ );
33565
33310
  }
33566
33311
  function describeMethod(init) {
33567
33312
  return typeof init.method === "string" && init.method.length > 0 ? init.method : "GET";
33568
33313
  }
33314
+ var MAX_REDIRECTS = 5;
33315
+ function openRawStream(url2, signal, redirectsLeft) {
33316
+ return new Promise(function attempt(resolve5, reject) {
33317
+ const target = new URL(url2);
33318
+ const send = target.protocol === "http:" ? http.request : https.request;
33319
+ const request = send(target, { method: "GET", headers: baseHeaders(), signal, agent: false }, function onResponse(response) {
33320
+ const status = response.statusCode;
33321
+ const location = response.headers.location;
33322
+ if (status >= 300 && status < 400 && location) {
33323
+ response.resume();
33324
+ if (redirectsLeft <= 0) {
33325
+ reject(new Error("GET " + url2 + " exceeded " + MAX_REDIRECTS + " redirects"));
33326
+ return;
33327
+ }
33328
+ resolve5(openRawStream(new URL(location, target).toString(), signal, redirectsLeft - 1));
33329
+ return;
33330
+ }
33331
+ resolve5({ status, headers: response.headers, stream: response });
33332
+ });
33333
+ request.on("error", reject);
33334
+ request.end();
33335
+ });
33336
+ }
33569
33337
  async function getTextConditional(url2, etag, options) {
33570
33338
  const headers = baseHeaders();
33571
33339
  if (etag) headers["If-None-Match"] = etag;
@@ -33609,21 +33377,33 @@ async function headFile(url2, options) {
33609
33377
  };
33610
33378
  }
33611
33379
  async function openDownloadStream(url2, onProgress, options) {
33612
- const response = await fetchWithRetry(url2, { headers: baseHeaders() }, options, DOWNLOAD_TIMEOUT_MS);
33613
- if (!response.ok) throw new Error("GET " + url2 + " failed: HTTP " + response.status);
33614
- if (!response.body) throw new Error("GET " + url2 + " returned no body");
33615
- const rawLength = response.headers.get("content-length");
33616
- const parsedLength = rawLength === null ? NaN : Number(rawLength);
33380
+ const response = await withRetry(
33381
+ "GET " + url2,
33382
+ function perform(signal) {
33383
+ return openRawStream(url2, signal, MAX_REDIRECTS);
33384
+ },
33385
+ function discard(raw) {
33386
+ raw.stream.resume();
33387
+ },
33388
+ options,
33389
+ DOWNLOAD_TIMEOUT_MS
33390
+ );
33391
+ if (response.status < 200 || response.status >= 300) {
33392
+ response.stream.resume();
33393
+ throw new Error("GET " + url2 + " failed: HTTP " + response.status);
33394
+ }
33395
+ const rawLength = response.headers["content-length"];
33396
+ const parsedLength = rawLength === void 0 ? NaN : Number(rawLength);
33617
33397
  const contentLength = Number.isFinite(parsedLength) ? parsedLength : null;
33618
- const stream$1 = stream.Readable.fromWeb(response.body);
33398
+ const stream = response.stream;
33619
33399
  if (onProgress) {
33620
33400
  let bytesRead = 0;
33621
- stream$1.on("data", function countChunk(chunk) {
33401
+ stream.on("data", function countChunk(chunk) {
33622
33402
  bytesRead += chunk.length;
33623
33403
  onProgress(bytesRead, contentLength);
33624
33404
  });
33625
33405
  }
33626
- return { stream: stream$1, contentLength, lastModified: response.headers.get("last-modified") };
33406
+ return { stream, contentLength, lastModified: response.headers["last-modified"] ?? null };
33627
33407
  }
33628
33408
 
33629
33409
  // ../../packages/feeds/src/osv/normalize.ts
@@ -33768,24 +33548,28 @@ async function* streamOsvSeed(ecosystem, onProgress, options) {
33768
33548
  const abortSignal = options && options.abortSignal;
33769
33549
  let batch = [];
33770
33550
  const zip = download.stream.pipe(import_unzipper.default.Parse({ forceStream: true }));
33771
- for await (const entry of zip) {
33772
- if (abortSignal && abortSignal.aborted) {
33773
- entry.autodrain();
33774
- throw new Error("aborted");
33775
- }
33776
- if (entry.type !== "File" || !String(entry.path).endsWith(".json")) {
33777
- entry.autodrain();
33778
- continue;
33551
+ try {
33552
+ for await (const entry of zip) {
33553
+ if (abortSignal && abortSignal.aborted) {
33554
+ entry.autodrain();
33555
+ throw new Error("aborted");
33556
+ }
33557
+ if (entry.type !== "File" || !String(entry.path).endsWith(".json")) {
33558
+ entry.autodrain();
33559
+ continue;
33560
+ }
33561
+ const content = await entry.buffer();
33562
+ for (const row of parseSeedEntry(content, ecosystem)) batch.push(row);
33563
+ if (batch.length >= BATCH_SIZE) {
33564
+ yield { rows: batch, lastModified: download.lastModified };
33565
+ batch = [];
33566
+ }
33779
33567
  }
33780
- const content = await entry.buffer();
33781
- for (const row of parseSeedEntry(content, ecosystem)) batch.push(row);
33782
- if (batch.length >= BATCH_SIZE) {
33568
+ if (batch.length > 0) {
33783
33569
  yield { rows: batch, lastModified: download.lastModified };
33784
- batch = [];
33785
33570
  }
33786
- }
33787
- if (batch.length > 0) {
33788
- yield { rows: batch, lastModified: download.lastModified };
33571
+ } finally {
33572
+ download.stream.destroy();
33789
33573
  }
33790
33574
  }
33791
33575
  async function fetchOsvChangedIds(ecosystem, cursorMs, etag, options) {
@@ -34073,23 +33857,28 @@ function nonEmpty(s) {
34073
33857
  }
34074
33858
 
34075
33859
  // ../../packages/feeds/src/gemnasium/feed.ts
34076
- var DEFAULT_ARCHIVE_URL = "https://gitlab.com/gitlab-org/security-products/gemnasium-db/-/archive/master/gemnasium-db-master.zip";
34077
33860
  var DEFAULT_API_BASE = "https://gitlab.com/api/v4/projects/gitlab-org%2Fsecurity-products%2Fgemnasium-db";
34078
33861
  var DEFAULT_REF = "master";
34079
33862
  var BATCH_SIZE2 = 2e3;
34080
33863
  var GEMNASIUM_COMPARE_MAX_FILES = 1e3;
34081
- function archiveUrl() {
33864
+ function archiveOverride() {
34082
33865
  const fromEnv = process.env.SENTINELLO_GEMNASIUM_FEED_URL;
34083
33866
  if (fromEnv && fromEnv.trim().length > 0) return fromEnv.trim();
34084
- return DEFAULT_ARCHIVE_URL;
33867
+ return null;
34085
33868
  }
34086
33869
  function apiBase() {
34087
33870
  const fromEnv = process.env.SENTINELLO_GEMNASIUM_API_URL;
34088
33871
  if (fromEnv && fromEnv.trim().length > 0) return fromEnv.trim();
34089
33872
  return DEFAULT_API_BASE;
34090
33873
  }
33874
+ function archiveUrl(ref) {
33875
+ const override = archiveOverride();
33876
+ if (override) return override;
33877
+ return apiBase() + "/repository/archive.zip?sha=" + encodeURIComponent(ref ?? DEFAULT_REF);
33878
+ }
34091
33879
  function gemnasiumFeedDisabled() {
34092
- return archiveUrl().toLowerCase() === "off";
33880
+ const override = archiveOverride();
33881
+ return override !== null && override.toLowerCase() === "off";
34093
33882
  }
34094
33883
  var PACKAGE_TYPE_TO_ECOSYSTEM = (function buildMap() {
34095
33884
  const map3 = {};
@@ -34163,30 +33952,34 @@ function advisoryIdFromPath(path) {
34163
33952
  const id = dot > 0 ? file2.slice(0, dot) : file2;
34164
33953
  return id.length > 0 ? id : null;
34165
33954
  }
34166
- async function* streamGemnasiumArchive(onProgress, options) {
34167
- const download = await openDownloadStream(archiveUrl(), onProgress, options);
33955
+ async function* streamGemnasiumArchive(ref, onProgress, options) {
33956
+ const download = await openDownloadStream(archiveUrl(ref), onProgress, options);
34168
33957
  const abortSignal = options && options.abortSignal;
34169
33958
  let batch = [];
34170
33959
  const zip = download.stream.pipe(import_unzipper2.default.Parse({ forceStream: true }));
34171
- for await (const entry of zip) {
34172
- if (abortSignal && abortSignal.aborted) {
34173
- entry.autodrain();
34174
- throw new Error("aborted");
34175
- }
34176
- const cell = advisoryPathEcosystem(String(entry.path), 1);
34177
- if (entry.type !== "File" || !cell) {
34178
- entry.autodrain();
34179
- continue;
33960
+ try {
33961
+ for await (const entry of zip) {
33962
+ if (abortSignal && abortSignal.aborted) {
33963
+ entry.autodrain();
33964
+ throw new Error("aborted");
33965
+ }
33966
+ const cell = advisoryPathEcosystem(String(entry.path), 1);
33967
+ if (entry.type !== "File" || !cell) {
33968
+ entry.autodrain();
33969
+ continue;
33970
+ }
33971
+ const content = await entry.buffer();
33972
+ for (const row of parseAdvisoryYaml(content, cell)) batch.push(row);
33973
+ if (batch.length >= BATCH_SIZE2) {
33974
+ yield { rows: batch, lastModified: download.lastModified };
33975
+ batch = [];
33976
+ }
34180
33977
  }
34181
- const content = await entry.buffer();
34182
- for (const row of parseAdvisoryYaml(content, cell)) batch.push(row);
34183
- if (batch.length >= BATCH_SIZE2) {
33978
+ if (batch.length > 0) {
34184
33979
  yield { rows: batch, lastModified: download.lastModified };
34185
- batch = [];
34186
33980
  }
34187
- }
34188
- if (batch.length > 0) {
34189
- yield { rows: batch, lastModified: download.lastModified };
33981
+ } finally {
33982
+ download.stream.destroy();
34190
33983
  }
34191
33984
  }
34192
33985
  function parseAdvisoryYaml(content, cell) {
@@ -34198,6 +33991,288 @@ function parseAdvisoryYaml(content, cell) {
34198
33991
  }
34199
33992
  return normalizeGemnasiumRecord(parsed, cell.ecosystem, cell.slugPrefix);
34200
33993
  }
33994
+ var CACHE_SCHEMA_VERSION = 1;
33995
+ function emptyMeta() {
33996
+ return { schemaVersion: CACHE_SCHEMA_VERSION, sources: { osv: {}, gemnasium: {} } };
33997
+ }
33998
+ function resolveCacheDir(override) {
33999
+ if (override && override.trim().length > 0) return path.resolve(override.trim());
34000
+ const fromEnv = process.env.SENTINELLO_CACHE_DIR;
34001
+ if (fromEnv && fromEnv.trim().length > 0) return path.resolve(fromEnv.trim());
34002
+ const xdg = process.env.XDG_CACHE_HOME;
34003
+ if (xdg && xdg.trim().length > 0) return path.join(path.resolve(xdg.trim()), "sentinello");
34004
+ return path.join(os.homedir(), ".cache", "sentinello");
34005
+ }
34006
+ function advisoryFilePath(cacheDir, source, ecosystem) {
34007
+ const slug = ecosystem.replace(/[^a-zA-Z0-9._-]+/g, "-");
34008
+ return path.join(cacheDir, source + "-" + slug + ".ndjson.gz");
34009
+ }
34010
+ function metaPath(cacheDir) {
34011
+ return path.join(cacheDir, "meta.json");
34012
+ }
34013
+ async function ensureCacheDir(cacheDir) {
34014
+ await promises.mkdir(cacheDir, { recursive: true });
34015
+ }
34016
+ async function readCacheMeta(cacheDir) {
34017
+ let text;
34018
+ try {
34019
+ text = await promises.readFile(metaPath(cacheDir), "utf8");
34020
+ } catch {
34021
+ return emptyMeta();
34022
+ }
34023
+ let parsed;
34024
+ try {
34025
+ parsed = JSON.parse(text);
34026
+ } catch {
34027
+ return emptyMeta();
34028
+ }
34029
+ if (!parsed || typeof parsed !== "object") return emptyMeta();
34030
+ const meta3 = parsed;
34031
+ if (meta3.schemaVersion !== CACHE_SCHEMA_VERSION) return emptyMeta();
34032
+ const sources = meta3.sources;
34033
+ if (!sources || typeof sources !== "object") return emptyMeta();
34034
+ return {
34035
+ schemaVersion: CACHE_SCHEMA_VERSION,
34036
+ sources: {
34037
+ osv: sources.osv ?? {},
34038
+ gemnasium: sources.gemnasium ?? {}
34039
+ }
34040
+ };
34041
+ }
34042
+ async function writeCacheMeta(cacheDir, meta3) {
34043
+ await ensureCacheDir(cacheDir);
34044
+ const target = metaPath(cacheDir);
34045
+ const tmp = target + ".tmp";
34046
+ await promises.writeFile(tmp, JSON.stringify(meta3, null, 4) + "\n", "utf8");
34047
+ await promises.rename(tmp, target);
34048
+ }
34049
+ function getSourceState(meta3, source, ecosystem) {
34050
+ return meta3.sources[source][ecosystem] ?? null;
34051
+ }
34052
+ function setSourceState(meta3, source, ecosystem, state) {
34053
+ meta3.sources[source][ecosystem] = state;
34054
+ }
34055
+ function isSeeded(meta3, source, ecosystem, normalizerVersion) {
34056
+ const state = getSourceState(meta3, source, ecosystem);
34057
+ if (!state) return false;
34058
+ return state.normalizerVersion === normalizerVersion && state.recordCount > 0;
34059
+ }
34060
+ async function tryAcquireLock(cacheDir) {
34061
+ await ensureCacheDir(cacheDir);
34062
+ const path$1 = path.join(cacheDir, ".lock");
34063
+ try {
34064
+ await promises.writeFile(path$1, String(process.pid), { flag: "wx" });
34065
+ } catch {
34066
+ if (await isStaleLock(path$1)) {
34067
+ await promises.rm(path$1, { force: true });
34068
+ return await tryAcquireLock(cacheDir);
34069
+ }
34070
+ return null;
34071
+ }
34072
+ async function release() {
34073
+ await promises.rm(path$1, { force: true });
34074
+ }
34075
+ return { release };
34076
+ }
34077
+ var LOCK_STALE_MS = 30 * 60 * 1e3;
34078
+ async function isStaleLock(path) {
34079
+ try {
34080
+ const info = await promises.stat(path);
34081
+ return Date.now() - info.mtimeMs > LOCK_STALE_MS;
34082
+ } catch {
34083
+ return false;
34084
+ }
34085
+ }
34086
+ var FIELD_SEPARATOR = " ";
34087
+ var LINE_SEPARATOR = "\n";
34088
+ function encodeRow(row) {
34089
+ const name = row.packageName.replace(/[\t\n\r]/g, "");
34090
+ const id = row.advisoryId.replace(/[\t\n\r]/g, "");
34091
+ return name + FIELD_SEPARATOR + id + FIELD_SEPARATOR + JSON.stringify(row);
34092
+ }
34093
+ function createRowWriter(path) {
34094
+ const tmpPath = path + ".tmp";
34095
+ const source = new stream.PassThrough();
34096
+ const gzip = zlib.createGzip();
34097
+ const done = promises$2.pipeline(source, gzip, fs.createWriteStream(tmpPath));
34098
+ let rowCount = 0;
34099
+ let failed = null;
34100
+ done.catch(function capture(err) {
34101
+ failed = err instanceof Error ? err : new Error(String(err));
34102
+ });
34103
+ async function push(chunk, added) {
34104
+ if (failed) throw failed;
34105
+ rowCount += added;
34106
+ if (!source.write(chunk)) {
34107
+ await new Promise(function waitForDrain(resolve5) {
34108
+ source.once("drain", resolve5);
34109
+ });
34110
+ }
34111
+ }
34112
+ async function write(rows) {
34113
+ if (rows.length === 0) return;
34114
+ await push(rows.map(encodeRow).join(LINE_SEPARATOR) + LINE_SEPARATOR, rows.length);
34115
+ }
34116
+ async function writeRaw(lines) {
34117
+ if (lines.length === 0) return;
34118
+ await push(lines.join(LINE_SEPARATOR) + LINE_SEPARATOR, lines.length);
34119
+ }
34120
+ async function commit() {
34121
+ source.end();
34122
+ await done;
34123
+ await promises.rename(tmpPath, path);
34124
+ return rowCount;
34125
+ }
34126
+ async function abort() {
34127
+ source.destroy();
34128
+ try {
34129
+ await done;
34130
+ } catch {
34131
+ }
34132
+ await promises.rm(tmpPath, { force: true });
34133
+ }
34134
+ return { write, writeRaw, commit, abort };
34135
+ }
34136
+ async function readRowsForPackages(path, packageNames) {
34137
+ const out = /* @__PURE__ */ new Map();
34138
+ if (packageNames.size === 0) return out;
34139
+ let raw;
34140
+ try {
34141
+ raw = await promises.readFile(path);
34142
+ } catch {
34143
+ return out;
34144
+ }
34145
+ const text = zlib.gunzipSync(raw).toString("utf8");
34146
+ let offset = 0;
34147
+ while (offset < text.length) {
34148
+ let end = text.indexOf(LINE_SEPARATOR, offset);
34149
+ if (end === -1) end = text.length;
34150
+ const nameEnd = text.indexOf(FIELD_SEPARATOR, offset);
34151
+ if (nameEnd === -1 || nameEnd > end) {
34152
+ offset = end + 1;
34153
+ continue;
34154
+ }
34155
+ const name = text.slice(offset, nameEnd);
34156
+ if (packageNames.has(name)) {
34157
+ const idEnd = text.indexOf(FIELD_SEPARATOR, nameEnd + 1);
34158
+ if (idEnd !== -1 && idEnd < end) {
34159
+ const parsed = safeParse3(text.slice(idEnd + 1, end));
34160
+ if (parsed) {
34161
+ const list = out.get(name);
34162
+ if (list) {
34163
+ list.push(parsed);
34164
+ } else {
34165
+ out.set(name, [parsed]);
34166
+ }
34167
+ }
34168
+ }
34169
+ }
34170
+ offset = end + 1;
34171
+ }
34172
+ return out;
34173
+ }
34174
+ async function rewriteRows(path, input) {
34175
+ let surviving = [];
34176
+ try {
34177
+ const raw = await promises.readFile(path);
34178
+ const text = zlib.gunzipSync(raw).toString("utf8");
34179
+ for (const line of text.split(LINE_SEPARATOR)) {
34180
+ if (line.length === 0) continue;
34181
+ const nameEnd = line.indexOf(FIELD_SEPARATOR);
34182
+ if (nameEnd === -1) continue;
34183
+ const idEnd = line.indexOf(FIELD_SEPARATOR, nameEnd + 1);
34184
+ if (idEnd === -1) continue;
34185
+ const advisoryId = line.slice(nameEnd + 1, idEnd);
34186
+ if (input.dropAdvisoryIds.has(advisoryId)) continue;
34187
+ surviving.push(line);
34188
+ }
34189
+ } catch {
34190
+ surviving = [];
34191
+ }
34192
+ const writer = createRowWriter(path);
34193
+ try {
34194
+ const CHUNK = 5e3;
34195
+ for (let i = 0; i < surviving.length; i += CHUNK) {
34196
+ await writer.writeRaw(surviving.slice(i, i + CHUNK));
34197
+ }
34198
+ await writer.write(input.append);
34199
+ return await writer.commit();
34200
+ } catch (err) {
34201
+ await writer.abort();
34202
+ throw err;
34203
+ }
34204
+ }
34205
+ async function countRows(path) {
34206
+ try {
34207
+ const raw = await promises.readFile(path);
34208
+ const text = zlib.gunzipSync(raw).toString("utf8");
34209
+ let count = 0;
34210
+ let offset = 0;
34211
+ for (; ; ) {
34212
+ const next = text.indexOf(LINE_SEPARATOR, offset);
34213
+ if (next === -1) break;
34214
+ count++;
34215
+ offset = next + 1;
34216
+ }
34217
+ return count;
34218
+ } catch {
34219
+ return 0;
34220
+ }
34221
+ }
34222
+ function safeParse3(json3) {
34223
+ try {
34224
+ return JSON.parse(json3);
34225
+ } catch {
34226
+ return null;
34227
+ }
34228
+ }
34229
+
34230
+ // src/cache/lookup.ts
34231
+ async function loadCacheForPackages(cacheDir, ecosystem, packageNames, sources) {
34232
+ const wanted = new Set(packageNames);
34233
+ const osv = /* @__PURE__ */ new Map();
34234
+ const gemnasium = /* @__PURE__ */ new Map();
34235
+ if (sources.includes("osv")) {
34236
+ const rows = await readRowsForPackages(advisoryFilePath(cacheDir, "osv", ecosystem), wanted);
34237
+ for (const [name, list] of rows.entries()) {
34238
+ osv.set(name, list.map(toOsvAdvisory));
34239
+ }
34240
+ }
34241
+ if (sources.includes("gemnasium")) {
34242
+ const rows = await readRowsForPackages(advisoryFilePath(cacheDir, "gemnasium", ecosystem), wanted);
34243
+ for (const [name, list] of rows.entries()) {
34244
+ gemnasium.set(name, list.map(toGemnasiumAdvisory));
34245
+ }
34246
+ }
34247
+ return { osv, gemnasium };
34248
+ }
34249
+ function cacheEcosystemKey(ecosystem) {
34250
+ const def = getEcosystem(ecosystem);
34251
+ return def ? def.osvEcosystem : ecosystem;
34252
+ }
34253
+ function toOsvAdvisory(row) {
34254
+ return {
34255
+ advisoryId: row.advisoryId,
34256
+ aliases: row.aliases,
34257
+ ranges: row.ranges,
34258
+ versions: row.versions,
34259
+ severity: row.severity,
34260
+ summary: row.summary,
34261
+ url: row.url,
34262
+ malicious: row.malicious
34263
+ };
34264
+ }
34265
+ function toGemnasiumAdvisory(row) {
34266
+ return {
34267
+ advisoryId: row.advisoryId,
34268
+ aliases: row.aliases,
34269
+ ranges: row.ranges,
34270
+ versions: row.versions,
34271
+ severity: row.severity,
34272
+ summary: row.summary,
34273
+ url: row.url
34274
+ };
34275
+ }
34201
34276
 
34202
34277
  // src/cache/sync.ts
34203
34278
  async function planSync(options) {
@@ -34388,7 +34463,7 @@ async function seedGemnasium(options, meta3, item) {
34388
34463
  const writer = createRowWriter(path);
34389
34464
  let rowCount;
34390
34465
  try {
34391
- for await (const batch of streamGemnasiumArchive(progressFor(options, item), fetchOptionsFor(options, item))) {
34466
+ for await (const batch of streamGemnasiumArchive(headSha, progressFor(options, item), fetchOptionsFor(options, item))) {
34392
34467
  const rows = batch.rows.filter(function forEcosystem(row) {
34393
34468
  return row.ecosystem === item.ecosystem;
34394
34469
  });
@@ -34847,6 +34922,13 @@ function renderJson(summary, options, generatedAt) {
34847
34922
  findings: summary.findings
34848
34923
  }, null, 2) + "\n";
34849
34924
  }
34925
+ function hasUnavailableSource(summary) {
34926
+ return summary.projects.some(function projectLostASource(project) {
34927
+ return project.unauditable.some(function lost(entry) {
34928
+ return isSourceUnavailableReason(entry.reasonCode);
34929
+ });
34930
+ });
34931
+ }
34850
34932
  function shouldFail(summary, failOn) {
34851
34933
  if (failOn === "none") return false;
34852
34934
  if (failOn === "any") return summary.totalFindings > 0;
@@ -34994,7 +35076,7 @@ var SEVERITY_ORDER = ["critical", "high", "moderate", "low", "info"];
34994
35076
  function formatBytes(bytes, estimated = false) {
34995
35077
  if (bytes === null) return "unknown size";
34996
35078
  const prefix = estimated ? "~" : "";
34997
- if (bytes < 1024) return prefix + bytes + " B";
35079
+ if (bytes < 1024) return prefix + Math.round(bytes) + " B";
34998
35080
  const mib = bytes / (1024 * 1024);
34999
35081
  if (mib < 1) return prefix + (bytes / 1024).toFixed(0) + " KB";
35000
35082
  if (mib < 1024) return prefix + mib.toFixed(1) + " MB";
@@ -35083,9 +35165,34 @@ function createUi(options) {
35083
35165
  rl.close();
35084
35166
  }
35085
35167
  }
35086
- function seedDeclined() {
35168
+ function sourcesSwitchedOff(sources) {
35169
+ const names = sources.map(sourceLabel).join(", ");
35170
+ write(" " + c.dim + "\xB7 " + names + " switched off and never seeded \u2014 not scanned" + c.reset);
35171
+ }
35172
+ async function confirmRetry(failed) {
35173
+ if (!interactive || !process.stdin.isTTY || !process.stderr.isTTY) return false;
35174
+ const names = failed.map(function label(outcome) {
35175
+ return sourceLabel(outcome.source);
35176
+ }).join(", ");
35177
+ write("");
35178
+ write(" " + c.yellow + names + " could not be downloaded." + c.reset);
35179
+ write(" " + c.dim + "The scan can continue without it, but anything only that source knows about" + c.reset);
35180
+ write(" " + c.dim + "will be missing from the report." + c.reset);
35181
+ const rl = promises$1.createInterface({ input: process.stdin, output: process.stderr });
35182
+ try {
35183
+ const answer = await rl.question(" Try the download again? " + c.dim + "[Y/n]" + c.reset + " ");
35184
+ const normalized = answer.trim().toLowerCase();
35185
+ return normalized === "" || normalized === "y" || normalized === "yes";
35186
+ } finally {
35187
+ rl.close();
35188
+ }
35189
+ }
35190
+ function seedDeclined(gated) {
35087
35191
  write("");
35088
35192
  write(" " + c.dim + "Skipped. Run again and accept, or pass --yes, to enable the advisory sources." + c.reset);
35193
+ if (gated) {
35194
+ write(" " + c.yellow + "--fail-on cannot be evaluated without them; exiting non-zero rather than reporting clean." + c.reset);
35195
+ }
35089
35196
  write("");
35090
35197
  }
35091
35198
  function offlineNotice() {
@@ -35177,9 +35284,25 @@ function createUi(options) {
35177
35284
  }
35178
35285
  write(line);
35179
35286
  }
35287
+ function lostSourceLines(runSummary) {
35288
+ const seen = /* @__PURE__ */ new Map();
35289
+ for (const project of runSummary.projects) {
35290
+ for (const entry of project.unauditable) {
35291
+ if (!isSourceUnavailableReason(entry.reasonCode)) continue;
35292
+ if (seen.has(entry.scanner)) continue;
35293
+ seen.set(entry.scanner, entry.errorText || entry.reasonCode);
35294
+ }
35295
+ }
35296
+ return Array.from(seen, function line([scanner, why]) {
35297
+ return sourceLabel(scanner) + c.dim + " " + why + c.reset;
35298
+ });
35299
+ }
35180
35300
  function summary(runSummary, destination) {
35181
35301
  write("");
35182
- if (runSummary.totalFindings === 0) {
35302
+ const lost = lostSourceLines(runSummary);
35303
+ if (runSummary.totalFindings === 0 && lost.length > 0) {
35304
+ write(" " + c.yellow + c.bold + "No findings \u2014 but not everything could be checked." + c.reset);
35305
+ } else if (runSummary.totalFindings === 0) {
35183
35306
  write(" " + c.green + c.bold + "No findings." + c.reset + c.dim + " " + runSummary.projects.length + " project" + (runSummary.projects.length === 1 ? "" : "s") + " clean." + c.reset);
35184
35307
  } else {
35185
35308
  const parts = [];
@@ -35200,6 +35323,13 @@ function createUi(options) {
35200
35323
  write(" " + project.relPath.padEnd(38).slice(0, 38) + " " + breakdown);
35201
35324
  }
35202
35325
  }
35326
+ if (lost.length > 0) {
35327
+ write("");
35328
+ for (const line of lost) write(" " + c.yellow + "!" + c.reset + " " + line);
35329
+ if (options.failOn !== "none") {
35330
+ write(" " + c.dim + "--fail-on cannot be honoured by a scan that lost a source; exiting non-zero." + c.reset);
35331
+ }
35332
+ }
35203
35333
  write("");
35204
35334
  if (destination) {
35205
35335
  write(" " + c.bold + "Advisory written to" + c.reset + " " + destination);
@@ -35220,6 +35350,8 @@ function createUi(options) {
35220
35350
  discovered,
35221
35351
  noProjects,
35222
35352
  confirmSeed,
35353
+ confirmRetry,
35354
+ sourcesSwitchedOff,
35223
35355
  seedDeclined,
35224
35356
  offlineNotice,
35225
35357
  syncStatus,
@@ -35236,8 +35368,8 @@ function createUi(options) {
35236
35368
 
35237
35369
  // src/help.ts
35238
35370
  var CLI_VERSION = (function resolveVersion() {
35239
- if ("3.0.0".length > 0) {
35240
- return "3.0.0";
35371
+ if ("3.0.1".length > 0) {
35372
+ return "3.0.1";
35241
35373
  }
35242
35374
  const fromEnv = process.env.SENTINELLO_VERSION;
35243
35375
  if (fromEnv && fromEnv.trim().length > 0) return fromEnv.trim();
@@ -35270,10 +35402,10 @@ SOURCES
35270
35402
  --offline Skip the freshness check and use the cached data as-is.
35271
35403
  --cache-dir <path> Where the advisory cache lives.
35272
35404
  Default: $XDG_CACHE_HOME/sentinello or ~/.cache/sentinello.
35273
- --feed-wait <secs> How long to keep waiting out a feed that declines a download
35274
- and clears on its own. GitLab does this to the gemnasium
35275
- archive for a minute or two at a time. Default: 180.
35276
- 0 gives up on the first refusal, leaving that source unseeded.
35405
+ --feed-wait <secs> How long to keep retrying a feed that declines a download.
35406
+ Default: 15 \u2014 short, because a declined download is offered
35407
+ as a retry rather than waited out. 0 gives up on the first
35408
+ refusal, leaving that source unseeded.
35277
35409
 
35278
35410
  OUTPUT
35279
35411
  --out <file|-> Write the advisory to a file, or "-" for stdout. With "-" the
@@ -35290,7 +35422,10 @@ OUTPUT
35290
35422
  BEHAVIOUR
35291
35423
  --fail-on <level> Exit 2 when a finding at this severity or above exists.
35292
35424
  A severity, "any", or "none". Default: none.
35293
- -y, --yes Accept every prompt, including the first-run download.
35425
+ -y, --yes Accept every prompt, including the first-run download. Required on a
35426
+ non-TTY: without it the download is refused, and under --fail-on
35427
+ that refusal exits 1 rather than reporting a clean scan. npx has a
35428
+ separate prompt of its own: in CI use npx --yes sentinello --yes
35294
35429
  --doctor Print cache status, resolved settings, and detected projects.
35295
35430
  -q, --quiet Suppress the human-readable output.
35296
35431
  --verbose List every directory skipped by an ignore rule.
@@ -35300,7 +35435,8 @@ BEHAVIOUR
35300
35435
 
35301
35436
  EXIT CODES
35302
35437
  0 completed (findings alone are not a failure)
35303
- 1 a scan or configuration error
35438
+ 1 a scan or configuration error, or a --fail-on run that lost an advisory source
35439
+ (a gate cannot be honoured by a scan that could not consult its sources)
35304
35440
  2 the --fail-on threshold was met
35305
35441
 
35306
35442
  CONFIG
@@ -35446,35 +35582,50 @@ async function runScan(options, cacheDir, ui) {
35446
35582
  ui.noProjects(skipped);
35447
35583
  return EXIT_OK;
35448
35584
  }
35449
- if (!options.offline && options.sources.length > 0) {
35450
- const plan = await planSync({ cacheDir, sources: options.sources, ecosystem: DEFAULT_ECOSYSTEM });
35585
+ const sources = enabledSources(options.sources, await readCacheMeta(cacheDir));
35586
+ const switchedOff = options.sources.filter(function isOff(source) {
35587
+ return !sources.includes(source);
35588
+ });
35589
+ if (switchedOff.length > 0) ui.sourcesSwitchedOff(switchedOff);
35590
+ if (!options.offline && sources.length > 0) {
35591
+ const plan = await planSync({ cacheDir, sources, ecosystem: DEFAULT_ECOSYSTEM });
35451
35592
  if (plan.needsConsent && !options.assumeYes) {
35452
35593
  const approved = await ui.confirmSeed(plan);
35453
35594
  if (!approved) {
35454
- ui.seedDeclined();
35455
- return EXIT_OK;
35595
+ const gated = options.failOn !== "none";
35596
+ ui.seedDeclined(gated);
35597
+ return gated ? EXIT_ERROR : EXIT_OK;
35456
35598
  }
35457
35599
  }
35458
- const outcomes = await runSync({
35600
+ const syncOptions = {
35459
35601
  cacheDir,
35460
- sources: options.sources,
35602
+ sources,
35461
35603
  ecosystem: DEFAULT_ECOSYSTEM,
35462
35604
  retryWaitMs: retryWaitMsFor(options.feedWaitSeconds),
35463
35605
  onProgress: ui.syncProgress,
35464
35606
  onRetry: ui.syncRetry,
35465
35607
  onStatus: ui.syncStatus
35466
- }, plan);
35608
+ };
35609
+ let outcomes = await runSync(syncOptions, plan);
35467
35610
  ui.syncDone(outcomes);
35611
+ for (; ; ) {
35612
+ const failed = failedItems(plan, outcomes);
35613
+ if (failed.length === 0) break;
35614
+ const approved = await ui.confirmRetry(outcomes.filter(isErrorOutcome));
35615
+ if (!approved) break;
35616
+ outcomes = await runSync(syncOptions, { items: failed, seedBytes: 0, needsConsent: false });
35617
+ ui.syncDone(outcomes);
35618
+ }
35468
35619
  } else if (options.offline) {
35469
35620
  ui.offlineNotice();
35470
35621
  }
35471
35622
  const resolved = await resolveProjects(projects);
35472
35623
  const packageNames = collectPackageNames(resolved);
35473
- const cache = await loadCacheForPackages(cacheDir, DEFAULT_ECOSYSTEM, packageNames, options.sources);
35624
+ const cache = await loadCacheForPackages(cacheDir, DEFAULT_ECOSYSTEM, packageNames, sources);
35474
35625
  const meta3 = await readCacheMeta(cacheDir);
35475
35626
  const setup = {
35476
35627
  cacheDir,
35477
- sources: options.sources,
35628
+ sources,
35478
35629
  ecosystem: DEFAULT_ECOSYSTEM,
35479
35630
  includeNpmAudit: options.includeNpmAudit,
35480
35631
  seeded: {
@@ -35508,8 +35659,33 @@ async function runScan(options, cacheDir, ui) {
35508
35659
  }
35509
35660
  ui.summary(summary, destination);
35510
35661
  if (shouldFail(summary, options.failOn)) return EXIT_THRESHOLD;
35662
+ if (options.failOn !== "none" && hasUnavailableSource(summary)) return EXIT_ERROR;
35511
35663
  return EXIT_OK;
35512
35664
  }
35665
+ function feedSwitchedOff(source) {
35666
+ if (source === "osv") return osvFeedDisabled();
35667
+ return gemnasiumFeedDisabled();
35668
+ }
35669
+ function normalizerVersionFor(source) {
35670
+ if (source === "osv") return OSV_NORMALIZER_VERSION;
35671
+ return GEMNASIUM_NORMALIZER_VERSION;
35672
+ }
35673
+ function enabledSources(sources, meta3) {
35674
+ return sources.filter(function isUsable(source) {
35675
+ if (!feedSwitchedOff(source)) return true;
35676
+ return isSeeded(meta3, source, DEFAULT_ECOSYSTEM, normalizerVersionFor(source));
35677
+ });
35678
+ }
35679
+ function isErrorOutcome(outcome) {
35680
+ return outcome.status === "error";
35681
+ }
35682
+ function failedItems(plan, outcomes) {
35683
+ return plan.items.filter(function wasError(item) {
35684
+ return outcomes.some(function matches(outcome) {
35685
+ return outcome.status === "error" && outcome.source === item.source && outcome.ecosystem === item.ecosystem;
35686
+ });
35687
+ });
35688
+ }
35513
35689
  function retryWaitMsFor(feedWaitSeconds) {
35514
35690
  if (feedWaitSeconds === null) return void 0;
35515
35691
  return feedWaitSeconds * 1e3;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sentinello",
3
- "version": "3.0.0",
3
+ "version": "3.0.1",
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": [