sentinello 3.0.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +7 -3
  2. package/dist/cli.cjs +576 -392
  3. package/package.json +1 -1
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');
@@ -25,7 +27,11 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
25
27
  throw Error('Dynamic require of "' + x + '" is not supported');
26
28
  });
27
29
  var __commonJS = (cb, mod) => function __require2() {
28
- return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
30
+ try {
31
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
32
+ } catch (e) {
33
+ throw mod = 0, e;
34
+ }
29
35
  };
30
36
  var __export = (target, all) => {
31
37
  for (var name in all)
@@ -3725,9 +3731,9 @@ var require_stream_duplex = __commonJS({
3725
3731
  module2.exports = Duplex;
3726
3732
  var util = Object.create(require_util());
3727
3733
  util.inherits = require_inherits();
3728
- var Readable2 = require_stream_readable();
3734
+ var Readable = require_stream_readable();
3729
3735
  var Writable = require_stream_writable();
3730
- util.inherits(Duplex, Readable2);
3736
+ util.inherits(Duplex, Readable);
3731
3737
  {
3732
3738
  keys = objectKeys(Writable.prototype);
3733
3739
  for (v = 0; v < keys.length; v++) {
@@ -3740,7 +3746,7 @@ var require_stream_duplex = __commonJS({
3740
3746
  var v;
3741
3747
  function Duplex(options) {
3742
3748
  if (!(this instanceof Duplex)) return new Duplex(options);
3743
- Readable2.call(this, options);
3749
+ Readable.call(this, options);
3744
3750
  Writable.call(this, options);
3745
3751
  if (options && options.readable === false) this.readable = false;
3746
3752
  if (options && options.writable === false) this.writable = false;
@@ -4028,10 +4034,10 @@ var require_string_decoder = __commonJS({
4028
4034
  var require_stream_readable = __commonJS({
4029
4035
  "../../node_modules/.pnpm/readable-stream@2.3.8/node_modules/readable-stream/lib/_stream_readable.js"(exports2, module2) {
4030
4036
  var pna = require_process_nextick_args();
4031
- module2.exports = Readable2;
4037
+ module2.exports = Readable;
4032
4038
  var isArray = require_isarray();
4033
4039
  var Duplex;
4034
- Readable2.ReadableState = ReadableState;
4040
+ Readable.ReadableState = ReadableState;
4035
4041
  __require("events").EventEmitter;
4036
4042
  var EElistenerCount = function(emitter, type2) {
4037
4043
  return emitter.listeners(type2).length;
@@ -4059,7 +4065,7 @@ var require_stream_readable = __commonJS({
4059
4065
  var BufferList = require_BufferList();
4060
4066
  var destroyImpl = require_destroy();
4061
4067
  var StringDecoder;
4062
- util.inherits(Readable2, Stream);
4068
+ util.inherits(Readable, Stream);
4063
4069
  var kProxyEvents = ["error", "close", "destroy", "pause", "resume"];
4064
4070
  function prependListener(emitter, event, fn) {
4065
4071
  if (typeof emitter.prependListener === "function") return emitter.prependListener(event, fn);
@@ -4105,9 +4111,9 @@ var require_stream_readable = __commonJS({
4105
4111
  this.encoding = options.encoding;
4106
4112
  }
4107
4113
  }
4108
- function Readable2(options) {
4114
+ function Readable(options) {
4109
4115
  Duplex = Duplex || require_stream_duplex();
4110
- if (!(this instanceof Readable2)) return new Readable2(options);
4116
+ if (!(this instanceof Readable)) return new Readable(options);
4111
4117
  this._readableState = new ReadableState(options, this);
4112
4118
  this.readable = true;
4113
4119
  if (options) {
@@ -4116,7 +4122,7 @@ var require_stream_readable = __commonJS({
4116
4122
  }
4117
4123
  Stream.call(this);
4118
4124
  }
4119
- Object.defineProperty(Readable2.prototype, "destroyed", {
4125
+ Object.defineProperty(Readable.prototype, "destroyed", {
4120
4126
  get: function() {
4121
4127
  if (this._readableState === void 0) {
4122
4128
  return false;
@@ -4130,13 +4136,13 @@ var require_stream_readable = __commonJS({
4130
4136
  this._readableState.destroyed = value;
4131
4137
  }
4132
4138
  });
4133
- Readable2.prototype.destroy = destroyImpl.destroy;
4134
- Readable2.prototype._undestroy = destroyImpl.undestroy;
4135
- Readable2.prototype._destroy = function(err, cb) {
4139
+ Readable.prototype.destroy = destroyImpl.destroy;
4140
+ Readable.prototype._undestroy = destroyImpl.undestroy;
4141
+ Readable.prototype._destroy = function(err, cb) {
4136
4142
  this.push(null);
4137
4143
  cb(err);
4138
4144
  };
4139
- Readable2.prototype.push = function(chunk, encoding) {
4145
+ Readable.prototype.push = function(chunk, encoding) {
4140
4146
  var state = this._readableState;
4141
4147
  var skipChunkCheck;
4142
4148
  if (!state.objectMode) {
@@ -4153,7 +4159,7 @@ var require_stream_readable = __commonJS({
4153
4159
  }
4154
4160
  return readableAddChunk(this, chunk, encoding, false, skipChunkCheck);
4155
4161
  };
4156
- Readable2.prototype.unshift = function(chunk) {
4162
+ Readable.prototype.unshift = function(chunk) {
4157
4163
  return readableAddChunk(this, chunk, null, true, false);
4158
4164
  };
4159
4165
  function readableAddChunk(stream, chunk, encoding, addToFront, skipChunkCheck) {
@@ -4213,10 +4219,10 @@ var require_stream_readable = __commonJS({
4213
4219
  function needMoreData(state) {
4214
4220
  return !state.ended && (state.needReadable || state.length < state.highWaterMark || state.length === 0);
4215
4221
  }
4216
- Readable2.prototype.isPaused = function() {
4222
+ Readable.prototype.isPaused = function() {
4217
4223
  return this._readableState.flowing === false;
4218
4224
  };
4219
- Readable2.prototype.setEncoding = function(enc) {
4225
+ Readable.prototype.setEncoding = function(enc) {
4220
4226
  if (!StringDecoder) StringDecoder = require_string_decoder().StringDecoder;
4221
4227
  this._readableState.decoder = new StringDecoder(enc);
4222
4228
  this._readableState.encoding = enc;
@@ -4252,7 +4258,7 @@ var require_stream_readable = __commonJS({
4252
4258
  }
4253
4259
  return state.length;
4254
4260
  }
4255
- Readable2.prototype.read = function(n) {
4261
+ Readable.prototype.read = function(n) {
4256
4262
  debug("read", n);
4257
4263
  n = parseInt(n, 10);
4258
4264
  var state = this._readableState;
@@ -4347,10 +4353,10 @@ var require_stream_readable = __commonJS({
4347
4353
  }
4348
4354
  state.readingMore = false;
4349
4355
  }
4350
- Readable2.prototype._read = function(n) {
4356
+ Readable.prototype._read = function(n) {
4351
4357
  this.emit("error", new Error("_read() is not implemented"));
4352
4358
  };
4353
- Readable2.prototype.pipe = function(dest, pipeOpts) {
4359
+ Readable.prototype.pipe = function(dest, pipeOpts) {
4354
4360
  var src = this;
4355
4361
  var state = this._readableState;
4356
4362
  switch (state.pipesCount) {
@@ -4455,7 +4461,7 @@ var require_stream_readable = __commonJS({
4455
4461
  }
4456
4462
  };
4457
4463
  }
4458
- Readable2.prototype.unpipe = function(dest) {
4464
+ Readable.prototype.unpipe = function(dest) {
4459
4465
  var state = this._readableState;
4460
4466
  var unpipeInfo = { hasUnpiped: false };
4461
4467
  if (state.pipesCount === 0) return this;
@@ -4487,7 +4493,7 @@ var require_stream_readable = __commonJS({
4487
4493
  dest.emit("unpipe", this, unpipeInfo);
4488
4494
  return this;
4489
4495
  };
4490
- Readable2.prototype.on = function(ev, fn) {
4496
+ Readable.prototype.on = function(ev, fn) {
4491
4497
  var res = Stream.prototype.on.call(this, ev, fn);
4492
4498
  if (ev === "data") {
4493
4499
  if (this._readableState.flowing !== false) this.resume();
@@ -4505,12 +4511,12 @@ var require_stream_readable = __commonJS({
4505
4511
  }
4506
4512
  return res;
4507
4513
  };
4508
- Readable2.prototype.addListener = Readable2.prototype.on;
4514
+ Readable.prototype.addListener = Readable.prototype.on;
4509
4515
  function nReadingNextTick(self2) {
4510
4516
  debug("readable nexttick read 0");
4511
4517
  self2.read(0);
4512
4518
  }
4513
- Readable2.prototype.resume = function() {
4519
+ Readable.prototype.resume = function() {
4514
4520
  var state = this._readableState;
4515
4521
  if (!state.flowing) {
4516
4522
  debug("resume");
@@ -4536,7 +4542,7 @@ var require_stream_readable = __commonJS({
4536
4542
  flow(stream);
4537
4543
  if (state.flowing && !state.reading) stream.read(0);
4538
4544
  }
4539
- Readable2.prototype.pause = function() {
4545
+ Readable.prototype.pause = function() {
4540
4546
  debug("call pause flowing=%j", this._readableState.flowing);
4541
4547
  if (false !== this._readableState.flowing) {
4542
4548
  debug("pause");
@@ -4551,7 +4557,7 @@ var require_stream_readable = __commonJS({
4551
4557
  while (state.flowing && stream.read() !== null) {
4552
4558
  }
4553
4559
  }
4554
- Readable2.prototype.wrap = function(stream) {
4560
+ Readable.prototype.wrap = function(stream) {
4555
4561
  var _this = this;
4556
4562
  var state = this._readableState;
4557
4563
  var paused = false;
@@ -4595,7 +4601,7 @@ var require_stream_readable = __commonJS({
4595
4601
  };
4596
4602
  return this;
4597
4603
  };
4598
- Object.defineProperty(Readable2.prototype, "readableHighWaterMark", {
4604
+ Object.defineProperty(Readable.prototype, "readableHighWaterMark", {
4599
4605
  // making it explicit this property is not enumerable
4600
4606
  // because otherwise some prototype manipulation in
4601
4607
  // userland will fail
@@ -4604,7 +4610,7 @@ var require_stream_readable = __commonJS({
4604
4610
  return this._readableState.highWaterMark;
4605
4611
  }
4606
4612
  });
4607
- Readable2._fromList = fromList;
4613
+ Readable._fromList = fromList;
4608
4614
  function fromList(n, state) {
4609
4615
  if (state.length === 0) return null;
4610
4616
  var ret2;
@@ -13117,6 +13123,15 @@ function compareSeverity(a, b) {
13117
13123
  function meetsSeverityFloor(severity, floor) {
13118
13124
  return severityWeight(severity) >= severityWeight(floor);
13119
13125
  }
13126
+ var SOURCE_UNAVAILABLE_REASON_CODES = [
13127
+ "osv_db_not_seeded",
13128
+ "osv_db_unavailable",
13129
+ "gemnasium_db_not_seeded",
13130
+ "gemnasium_db_unavailable"
13131
+ ];
13132
+ function isSourceUnavailableReason(code) {
13133
+ return SOURCE_UNAVAILABLE_REASON_CODES.includes(code);
13134
+ }
13120
13135
 
13121
13136
  // ../../packages/core/src/advisory-rows.ts
13122
13137
  var OSV_NORMALIZER_VERSION = 3;
@@ -28593,6 +28608,10 @@ async function detectYarnMajor(projectPath, lockfile) {
28593
28608
  }
28594
28609
  return null;
28595
28610
  }
28611
+ function auditSummary(ctx, findingCount) {
28612
+ const packageCount = ctx.resolvedGraph?.packages?.length ?? null;
28613
+ return JSON.stringify({ source: SCANNER_NAME, packageCount, findingCount });
28614
+ }
28596
28615
  function logCrossCheckDrops(result, packageManager) {
28597
28616
  if (result.droppedCount === 0) return;
28598
28617
  const MAX_LIST = 10;
@@ -28701,7 +28720,7 @@ async function runNpmAudit(projectPath, ctx, deps = REAL_DEPS) {
28701
28720
  status: "ok",
28702
28721
  reasonCode: "ok",
28703
28722
  findings: crossChecked2.kept,
28704
- rawJson: rawText,
28723
+ rawJson: auditSummary(ctx, crossChecked2.kept.length),
28705
28724
  errorText: null,
28706
28725
  durationMs: Date.now() - startedAt
28707
28726
  };
@@ -28725,7 +28744,7 @@ async function runNpmAudit(projectPath, ctx, deps = REAL_DEPS) {
28725
28744
  status: "ok",
28726
28745
  reasonCode: "ok",
28727
28746
  findings: crossChecked.kept,
28728
- rawJson: rawText,
28747
+ rawJson: auditSummary(ctx, crossChecked.kept.length),
28729
28748
  errorText: null,
28730
28749
  durationMs: Date.now() - startedAt
28731
28750
  };
@@ -28986,7 +29005,7 @@ function nameFromNodePath(nodePath) {
28986
29005
  return slash < 0 ? tail : tail.slice(0, slash);
28987
29006
  }
28988
29007
 
28989
- // ../../node_modules/.pnpm/js-yaml@4.3.0/node_modules/js-yaml/dist/js-yaml.mjs
29008
+ // ../../node_modules/.pnpm/js-yaml@4.3.1/node_modules/js-yaml/dist/js-yaml.mjs
28990
29009
  function getDefaultExportFromCjs(x) {
28991
29010
  return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, "default") ? x["default"] : x;
28992
29011
  }
@@ -29847,7 +29866,7 @@ function requireOmap() {
29847
29866
  const _toString = Object.prototype.toString;
29848
29867
  function resolveYamlOmap(data) {
29849
29868
  if (data === null) return true;
29850
- const objectKeys = [];
29869
+ const objectKeys = {};
29851
29870
  const object2 = data;
29852
29871
  for (let index = 0, length = object2.length; index < length; index += 1) {
29853
29872
  const pair = object2[index];
@@ -29861,8 +29880,8 @@ function requireOmap() {
29861
29880
  }
29862
29881
  }
29863
29882
  if (!pairHasKey) return false;
29864
- if (objectKeys.indexOf(pairKey) === -1) objectKeys.push(pairKey);
29865
- else return false;
29883
+ if (_hasOwnProperty.call(objectKeys, pairKey)) return false;
29884
+ Object.defineProperty(objectKeys, pairKey, { value: true });
29866
29885
  }
29867
29886
  return true;
29868
29887
  }
@@ -33206,288 +33225,6 @@ function reconcileAgainstReported(findings, reportedByPackage) {
33206
33225
  // ../../packages/scanners/src/index.ts
33207
33226
  var registry2 = /* @__PURE__ */ new Map();
33208
33227
  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
33228
 
33492
33229
  // ../../packages/feeds/src/osv/feed.ts
33493
33230
  var import_unzipper = __toESM(require_unzip2());
@@ -33521,8 +33258,8 @@ function isSlowTransientStatus(status) {
33521
33258
  var FAST_MAX_ATTEMPTS = 4;
33522
33259
  var RETRY_BASE_MS = 1e3;
33523
33260
  var RETRY_FACTOR = 3;
33524
- var DEFAULT_RETRY_WAIT_MS = 18e4;
33525
- var SLOW_RETRY_WAITS_MS = [45e3, 6e4, 75e3];
33261
+ var DEFAULT_RETRY_WAIT_MS = 15e3;
33262
+ var SLOW_RETRY_WAITS_MS = [2e3, 5e3, 8e3];
33526
33263
  function delay(ms) {
33527
33264
  return new Promise(function schedule(resolve5) {
33528
33265
  setTimeout(resolve5, ms);
@@ -33532,7 +33269,7 @@ function retryBudgetMs(options) {
33532
33269
  if (options && typeof options.retryWaitMs === "number" && options.retryWaitMs >= 0) return options.retryWaitMs;
33533
33270
  return DEFAULT_RETRY_WAIT_MS;
33534
33271
  }
33535
- async function fetchWithRetry(url2, init, options, timeoutMs) {
33272
+ async function withRetry(describe3, perform, discard, options, timeoutMs) {
33536
33273
  const budgetMs = retryBudgetMs(options);
33537
33274
  const startedAt = Date.now();
33538
33275
  let lastStatus;
@@ -33541,19 +33278,19 @@ async function fetchWithRetry(url2, init, options, timeoutMs) {
33541
33278
  let slowAttempts = 0;
33542
33279
  for (; ; ) {
33543
33280
  attempt++;
33544
- const response = await fetch(url2, { ...init, signal: signalFor(options, timeoutMs) });
33281
+ const response = await perform(signalFor(options, timeoutMs));
33545
33282
  const slow = isSlowTransientStatus(response.status);
33546
33283
  if (!slow && !isFastTransientStatus(response.status)) return response;
33547
33284
  lastStatus = response.status;
33548
- if (response.body) await response.body.cancel();
33285
+ await discard(response);
33549
33286
  if (options && options.abortSignal && options.abortSignal.aborted) break;
33550
33287
  if (slow) {
33551
- const elapsedMs = Date.now() - startedAt;
33552
- if (elapsedMs >= budgetMs) break;
33288
+ const elapsedMs2 = Date.now() - startedAt;
33289
+ if (elapsedMs2 >= budgetMs) break;
33553
33290
  const nominalMs = SLOW_RETRY_WAITS_MS[Math.min(slowAttempts, SLOW_RETRY_WAITS_MS.length - 1)];
33554
- const waitMs = Math.min(nominalMs, budgetMs - elapsedMs);
33291
+ const waitMs = Math.min(nominalMs, budgetMs - elapsedMs2);
33555
33292
  slowAttempts++;
33556
- if (options && options.onRetry) options.onRetry({ status: lastStatus, attempt, waitMs, elapsedMs, budgetMs });
33293
+ if (options && options.onRetry) options.onRetry({ status: lastStatus, attempt, waitMs, elapsedMs: elapsedMs2, budgetMs });
33557
33294
  await delay(waitMs);
33558
33295
  continue;
33559
33296
  }
@@ -33561,11 +33298,50 @@ async function fetchWithRetry(url2, init, options, timeoutMs) {
33561
33298
  if (fastAttempts >= FAST_MAX_ATTEMPTS) break;
33562
33299
  await delay(RETRY_BASE_MS * Math.pow(RETRY_FACTOR, fastAttempts - 1));
33563
33300
  }
33564
- throw new Error(describeMethod(init) + " " + url2 + " failed: HTTP " + lastStatus + " after " + attempt + " attempts");
33301
+ const elapsedMs = Date.now() - startedAt;
33302
+ throw new Error(
33303
+ describe3 + " failed: HTTP " + lastStatus + " after " + attempt + " attempts over " + Math.round(elapsedMs / 1e3) + "s"
33304
+ );
33305
+ }
33306
+ async function fetchWithRetry(url2, init, options, timeoutMs) {
33307
+ return await withRetry(
33308
+ describeMethod(init) + " " + url2,
33309
+ function perform(signal) {
33310
+ return fetch(url2, { ...init, signal });
33311
+ },
33312
+ async function discard(response) {
33313
+ if (response.body) await response.body.cancel();
33314
+ },
33315
+ options,
33316
+ timeoutMs
33317
+ );
33565
33318
  }
33566
33319
  function describeMethod(init) {
33567
33320
  return typeof init.method === "string" && init.method.length > 0 ? init.method : "GET";
33568
33321
  }
33322
+ var MAX_REDIRECTS = 5;
33323
+ function openRawStream(url2, signal, redirectsLeft) {
33324
+ return new Promise(function attempt(resolve5, reject) {
33325
+ const target = new URL(url2);
33326
+ const send = target.protocol === "http:" ? http.request : https.request;
33327
+ const request = send(target, { method: "GET", headers: baseHeaders(), signal, agent: false }, function onResponse(response) {
33328
+ const status = response.statusCode;
33329
+ const location = response.headers.location;
33330
+ if (status >= 300 && status < 400 && location) {
33331
+ response.resume();
33332
+ if (redirectsLeft <= 0) {
33333
+ reject(new Error("GET " + url2 + " exceeded " + MAX_REDIRECTS + " redirects"));
33334
+ return;
33335
+ }
33336
+ resolve5(openRawStream(new URL(location, target).toString(), signal, redirectsLeft - 1));
33337
+ return;
33338
+ }
33339
+ resolve5({ status, headers: response.headers, stream: response });
33340
+ });
33341
+ request.on("error", reject);
33342
+ request.end();
33343
+ });
33344
+ }
33569
33345
  async function getTextConditional(url2, etag, options) {
33570
33346
  const headers = baseHeaders();
33571
33347
  if (etag) headers["If-None-Match"] = etag;
@@ -33609,21 +33385,33 @@ async function headFile(url2, options) {
33609
33385
  };
33610
33386
  }
33611
33387
  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);
33388
+ const response = await withRetry(
33389
+ "GET " + url2,
33390
+ function perform(signal) {
33391
+ return openRawStream(url2, signal, MAX_REDIRECTS);
33392
+ },
33393
+ function discard(raw) {
33394
+ raw.stream.resume();
33395
+ },
33396
+ options,
33397
+ DOWNLOAD_TIMEOUT_MS
33398
+ );
33399
+ if (response.status < 200 || response.status >= 300) {
33400
+ response.stream.resume();
33401
+ throw new Error("GET " + url2 + " failed: HTTP " + response.status);
33402
+ }
33403
+ const rawLength = response.headers["content-length"];
33404
+ const parsedLength = rawLength === void 0 ? NaN : Number(rawLength);
33617
33405
  const contentLength = Number.isFinite(parsedLength) ? parsedLength : null;
33618
- const stream$1 = stream.Readable.fromWeb(response.body);
33406
+ const stream = response.stream;
33619
33407
  if (onProgress) {
33620
33408
  let bytesRead = 0;
33621
- stream$1.on("data", function countChunk(chunk) {
33409
+ stream.on("data", function countChunk(chunk) {
33622
33410
  bytesRead += chunk.length;
33623
33411
  onProgress(bytesRead, contentLength);
33624
33412
  });
33625
33413
  }
33626
- return { stream: stream$1, contentLength, lastModified: response.headers.get("last-modified") };
33414
+ return { stream, contentLength, lastModified: response.headers["last-modified"] ?? null };
33627
33415
  }
33628
33416
 
33629
33417
  // ../../packages/feeds/src/osv/normalize.ts
@@ -33768,24 +33556,28 @@ async function* streamOsvSeed(ecosystem, onProgress, options) {
33768
33556
  const abortSignal = options && options.abortSignal;
33769
33557
  let batch = [];
33770
33558
  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;
33559
+ try {
33560
+ for await (const entry of zip) {
33561
+ if (abortSignal && abortSignal.aborted) {
33562
+ entry.autodrain();
33563
+ throw new Error("aborted");
33564
+ }
33565
+ if (entry.type !== "File" || !String(entry.path).endsWith(".json")) {
33566
+ entry.autodrain();
33567
+ continue;
33568
+ }
33569
+ const content = await entry.buffer();
33570
+ for (const row of parseSeedEntry(content, ecosystem)) batch.push(row);
33571
+ if (batch.length >= BATCH_SIZE) {
33572
+ yield { rows: batch, lastModified: download.lastModified };
33573
+ batch = [];
33574
+ }
33779
33575
  }
33780
- const content = await entry.buffer();
33781
- for (const row of parseSeedEntry(content, ecosystem)) batch.push(row);
33782
- if (batch.length >= BATCH_SIZE) {
33576
+ if (batch.length > 0) {
33783
33577
  yield { rows: batch, lastModified: download.lastModified };
33784
- batch = [];
33785
33578
  }
33786
- }
33787
- if (batch.length > 0) {
33788
- yield { rows: batch, lastModified: download.lastModified };
33579
+ } finally {
33580
+ download.stream.destroy();
33789
33581
  }
33790
33582
  }
33791
33583
  async function fetchOsvChangedIds(ecosystem, cursorMs, etag, options) {
@@ -34073,23 +33865,28 @@ function nonEmpty(s) {
34073
33865
  }
34074
33866
 
34075
33867
  // ../../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
33868
  var DEFAULT_API_BASE = "https://gitlab.com/api/v4/projects/gitlab-org%2Fsecurity-products%2Fgemnasium-db";
34078
33869
  var DEFAULT_REF = "master";
34079
33870
  var BATCH_SIZE2 = 2e3;
34080
33871
  var GEMNASIUM_COMPARE_MAX_FILES = 1e3;
34081
- function archiveUrl() {
33872
+ function archiveOverride() {
34082
33873
  const fromEnv = process.env.SENTINELLO_GEMNASIUM_FEED_URL;
34083
33874
  if (fromEnv && fromEnv.trim().length > 0) return fromEnv.trim();
34084
- return DEFAULT_ARCHIVE_URL;
33875
+ return null;
34085
33876
  }
34086
33877
  function apiBase() {
34087
33878
  const fromEnv = process.env.SENTINELLO_GEMNASIUM_API_URL;
34088
33879
  if (fromEnv && fromEnv.trim().length > 0) return fromEnv.trim();
34089
33880
  return DEFAULT_API_BASE;
34090
33881
  }
33882
+ function archiveUrl(ref) {
33883
+ const override = archiveOverride();
33884
+ if (override) return override;
33885
+ return apiBase() + "/repository/archive.zip?sha=" + encodeURIComponent(ref ?? DEFAULT_REF);
33886
+ }
34091
33887
  function gemnasiumFeedDisabled() {
34092
- return archiveUrl().toLowerCase() === "off";
33888
+ const override = archiveOverride();
33889
+ return override !== null && override.toLowerCase() === "off";
34093
33890
  }
34094
33891
  var PACKAGE_TYPE_TO_ECOSYSTEM = (function buildMap() {
34095
33892
  const map3 = {};
@@ -34163,30 +33960,34 @@ function advisoryIdFromPath(path) {
34163
33960
  const id = dot > 0 ? file2.slice(0, dot) : file2;
34164
33961
  return id.length > 0 ? id : null;
34165
33962
  }
34166
- async function* streamGemnasiumArchive(onProgress, options) {
34167
- const download = await openDownloadStream(archiveUrl(), onProgress, options);
33963
+ async function* streamGemnasiumArchive(ref, onProgress, options) {
33964
+ const download = await openDownloadStream(archiveUrl(ref), onProgress, options);
34168
33965
  const abortSignal = options && options.abortSignal;
34169
33966
  let batch = [];
34170
33967
  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;
33968
+ try {
33969
+ for await (const entry of zip) {
33970
+ if (abortSignal && abortSignal.aborted) {
33971
+ entry.autodrain();
33972
+ throw new Error("aborted");
33973
+ }
33974
+ const cell = advisoryPathEcosystem(String(entry.path), 1);
33975
+ if (entry.type !== "File" || !cell) {
33976
+ entry.autodrain();
33977
+ continue;
33978
+ }
33979
+ const content = await entry.buffer();
33980
+ for (const row of parseAdvisoryYaml(content, cell)) batch.push(row);
33981
+ if (batch.length >= BATCH_SIZE2) {
33982
+ yield { rows: batch, lastModified: download.lastModified };
33983
+ batch = [];
33984
+ }
34180
33985
  }
34181
- const content = await entry.buffer();
34182
- for (const row of parseAdvisoryYaml(content, cell)) batch.push(row);
34183
- if (batch.length >= BATCH_SIZE2) {
33986
+ if (batch.length > 0) {
34184
33987
  yield { rows: batch, lastModified: download.lastModified };
34185
- batch = [];
34186
33988
  }
34187
- }
34188
- if (batch.length > 0) {
34189
- yield { rows: batch, lastModified: download.lastModified };
33989
+ } finally {
33990
+ download.stream.destroy();
34190
33991
  }
34191
33992
  }
34192
33993
  function parseAdvisoryYaml(content, cell) {
@@ -34198,6 +33999,288 @@ function parseAdvisoryYaml(content, cell) {
34198
33999
  }
34199
34000
  return normalizeGemnasiumRecord(parsed, cell.ecosystem, cell.slugPrefix);
34200
34001
  }
34002
+ var CACHE_SCHEMA_VERSION = 1;
34003
+ function emptyMeta() {
34004
+ return { schemaVersion: CACHE_SCHEMA_VERSION, sources: { osv: {}, gemnasium: {} } };
34005
+ }
34006
+ function resolveCacheDir(override) {
34007
+ if (override && override.trim().length > 0) return path.resolve(override.trim());
34008
+ const fromEnv = process.env.SENTINELLO_CACHE_DIR;
34009
+ if (fromEnv && fromEnv.trim().length > 0) return path.resolve(fromEnv.trim());
34010
+ const xdg = process.env.XDG_CACHE_HOME;
34011
+ if (xdg && xdg.trim().length > 0) return path.join(path.resolve(xdg.trim()), "sentinello");
34012
+ return path.join(os.homedir(), ".cache", "sentinello");
34013
+ }
34014
+ function advisoryFilePath(cacheDir, source, ecosystem) {
34015
+ const slug = ecosystem.replace(/[^a-zA-Z0-9._-]+/g, "-");
34016
+ return path.join(cacheDir, source + "-" + slug + ".ndjson.gz");
34017
+ }
34018
+ function metaPath(cacheDir) {
34019
+ return path.join(cacheDir, "meta.json");
34020
+ }
34021
+ async function ensureCacheDir(cacheDir) {
34022
+ await promises.mkdir(cacheDir, { recursive: true });
34023
+ }
34024
+ async function readCacheMeta(cacheDir) {
34025
+ let text;
34026
+ try {
34027
+ text = await promises.readFile(metaPath(cacheDir), "utf8");
34028
+ } catch {
34029
+ return emptyMeta();
34030
+ }
34031
+ let parsed;
34032
+ try {
34033
+ parsed = JSON.parse(text);
34034
+ } catch {
34035
+ return emptyMeta();
34036
+ }
34037
+ if (!parsed || typeof parsed !== "object") return emptyMeta();
34038
+ const meta3 = parsed;
34039
+ if (meta3.schemaVersion !== CACHE_SCHEMA_VERSION) return emptyMeta();
34040
+ const sources = meta3.sources;
34041
+ if (!sources || typeof sources !== "object") return emptyMeta();
34042
+ return {
34043
+ schemaVersion: CACHE_SCHEMA_VERSION,
34044
+ sources: {
34045
+ osv: sources.osv ?? {},
34046
+ gemnasium: sources.gemnasium ?? {}
34047
+ }
34048
+ };
34049
+ }
34050
+ async function writeCacheMeta(cacheDir, meta3) {
34051
+ await ensureCacheDir(cacheDir);
34052
+ const target = metaPath(cacheDir);
34053
+ const tmp = target + ".tmp";
34054
+ await promises.writeFile(tmp, JSON.stringify(meta3, null, 4) + "\n", "utf8");
34055
+ await promises.rename(tmp, target);
34056
+ }
34057
+ function getSourceState(meta3, source, ecosystem) {
34058
+ return meta3.sources[source][ecosystem] ?? null;
34059
+ }
34060
+ function setSourceState(meta3, source, ecosystem, state) {
34061
+ meta3.sources[source][ecosystem] = state;
34062
+ }
34063
+ function isSeeded(meta3, source, ecosystem, normalizerVersion) {
34064
+ const state = getSourceState(meta3, source, ecosystem);
34065
+ if (!state) return false;
34066
+ return state.normalizerVersion === normalizerVersion && state.recordCount > 0;
34067
+ }
34068
+ async function tryAcquireLock(cacheDir) {
34069
+ await ensureCacheDir(cacheDir);
34070
+ const path$1 = path.join(cacheDir, ".lock");
34071
+ try {
34072
+ await promises.writeFile(path$1, String(process.pid), { flag: "wx" });
34073
+ } catch {
34074
+ if (await isStaleLock(path$1)) {
34075
+ await promises.rm(path$1, { force: true });
34076
+ return await tryAcquireLock(cacheDir);
34077
+ }
34078
+ return null;
34079
+ }
34080
+ async function release() {
34081
+ await promises.rm(path$1, { force: true });
34082
+ }
34083
+ return { release };
34084
+ }
34085
+ var LOCK_STALE_MS = 30 * 60 * 1e3;
34086
+ async function isStaleLock(path) {
34087
+ try {
34088
+ const info = await promises.stat(path);
34089
+ return Date.now() - info.mtimeMs > LOCK_STALE_MS;
34090
+ } catch {
34091
+ return false;
34092
+ }
34093
+ }
34094
+ var FIELD_SEPARATOR = " ";
34095
+ var LINE_SEPARATOR = "\n";
34096
+ function encodeRow(row) {
34097
+ const name = row.packageName.replace(/[\t\n\r]/g, "");
34098
+ const id = row.advisoryId.replace(/[\t\n\r]/g, "");
34099
+ return name + FIELD_SEPARATOR + id + FIELD_SEPARATOR + JSON.stringify(row);
34100
+ }
34101
+ function createRowWriter(path) {
34102
+ const tmpPath = path + ".tmp";
34103
+ const source = new stream.PassThrough();
34104
+ const gzip = zlib.createGzip();
34105
+ const done = promises$2.pipeline(source, gzip, fs.createWriteStream(tmpPath));
34106
+ let rowCount = 0;
34107
+ let failed = null;
34108
+ done.catch(function capture(err) {
34109
+ failed = err instanceof Error ? err : new Error(String(err));
34110
+ });
34111
+ async function push(chunk, added) {
34112
+ if (failed) throw failed;
34113
+ rowCount += added;
34114
+ if (!source.write(chunk)) {
34115
+ await new Promise(function waitForDrain(resolve5) {
34116
+ source.once("drain", resolve5);
34117
+ });
34118
+ }
34119
+ }
34120
+ async function write(rows) {
34121
+ if (rows.length === 0) return;
34122
+ await push(rows.map(encodeRow).join(LINE_SEPARATOR) + LINE_SEPARATOR, rows.length);
34123
+ }
34124
+ async function writeRaw(lines) {
34125
+ if (lines.length === 0) return;
34126
+ await push(lines.join(LINE_SEPARATOR) + LINE_SEPARATOR, lines.length);
34127
+ }
34128
+ async function commit() {
34129
+ source.end();
34130
+ await done;
34131
+ await promises.rename(tmpPath, path);
34132
+ return rowCount;
34133
+ }
34134
+ async function abort() {
34135
+ source.destroy();
34136
+ try {
34137
+ await done;
34138
+ } catch {
34139
+ }
34140
+ await promises.rm(tmpPath, { force: true });
34141
+ }
34142
+ return { write, writeRaw, commit, abort };
34143
+ }
34144
+ async function readRowsForPackages(path, packageNames) {
34145
+ const out = /* @__PURE__ */ new Map();
34146
+ if (packageNames.size === 0) return out;
34147
+ let raw;
34148
+ try {
34149
+ raw = await promises.readFile(path);
34150
+ } catch {
34151
+ return out;
34152
+ }
34153
+ const text = zlib.gunzipSync(raw).toString("utf8");
34154
+ let offset = 0;
34155
+ while (offset < text.length) {
34156
+ let end = text.indexOf(LINE_SEPARATOR, offset);
34157
+ if (end === -1) end = text.length;
34158
+ const nameEnd = text.indexOf(FIELD_SEPARATOR, offset);
34159
+ if (nameEnd === -1 || nameEnd > end) {
34160
+ offset = end + 1;
34161
+ continue;
34162
+ }
34163
+ const name = text.slice(offset, nameEnd);
34164
+ if (packageNames.has(name)) {
34165
+ const idEnd = text.indexOf(FIELD_SEPARATOR, nameEnd + 1);
34166
+ if (idEnd !== -1 && idEnd < end) {
34167
+ const parsed = safeParse3(text.slice(idEnd + 1, end));
34168
+ if (parsed) {
34169
+ const list = out.get(name);
34170
+ if (list) {
34171
+ list.push(parsed);
34172
+ } else {
34173
+ out.set(name, [parsed]);
34174
+ }
34175
+ }
34176
+ }
34177
+ }
34178
+ offset = end + 1;
34179
+ }
34180
+ return out;
34181
+ }
34182
+ async function rewriteRows(path, input) {
34183
+ let surviving = [];
34184
+ try {
34185
+ const raw = await promises.readFile(path);
34186
+ const text = zlib.gunzipSync(raw).toString("utf8");
34187
+ for (const line of text.split(LINE_SEPARATOR)) {
34188
+ if (line.length === 0) continue;
34189
+ const nameEnd = line.indexOf(FIELD_SEPARATOR);
34190
+ if (nameEnd === -1) continue;
34191
+ const idEnd = line.indexOf(FIELD_SEPARATOR, nameEnd + 1);
34192
+ if (idEnd === -1) continue;
34193
+ const advisoryId = line.slice(nameEnd + 1, idEnd);
34194
+ if (input.dropAdvisoryIds.has(advisoryId)) continue;
34195
+ surviving.push(line);
34196
+ }
34197
+ } catch {
34198
+ surviving = [];
34199
+ }
34200
+ const writer = createRowWriter(path);
34201
+ try {
34202
+ const CHUNK = 5e3;
34203
+ for (let i = 0; i < surviving.length; i += CHUNK) {
34204
+ await writer.writeRaw(surviving.slice(i, i + CHUNK));
34205
+ }
34206
+ await writer.write(input.append);
34207
+ return await writer.commit();
34208
+ } catch (err) {
34209
+ await writer.abort();
34210
+ throw err;
34211
+ }
34212
+ }
34213
+ async function countRows(path) {
34214
+ try {
34215
+ const raw = await promises.readFile(path);
34216
+ const text = zlib.gunzipSync(raw).toString("utf8");
34217
+ let count = 0;
34218
+ let offset = 0;
34219
+ for (; ; ) {
34220
+ const next = text.indexOf(LINE_SEPARATOR, offset);
34221
+ if (next === -1) break;
34222
+ count++;
34223
+ offset = next + 1;
34224
+ }
34225
+ return count;
34226
+ } catch {
34227
+ return 0;
34228
+ }
34229
+ }
34230
+ function safeParse3(json3) {
34231
+ try {
34232
+ return JSON.parse(json3);
34233
+ } catch {
34234
+ return null;
34235
+ }
34236
+ }
34237
+
34238
+ // src/cache/lookup.ts
34239
+ async function loadCacheForPackages(cacheDir, ecosystem, packageNames, sources) {
34240
+ const wanted = new Set(packageNames);
34241
+ const osv = /* @__PURE__ */ new Map();
34242
+ const gemnasium = /* @__PURE__ */ new Map();
34243
+ if (sources.includes("osv")) {
34244
+ const rows = await readRowsForPackages(advisoryFilePath(cacheDir, "osv", ecosystem), wanted);
34245
+ for (const [name, list] of rows.entries()) {
34246
+ osv.set(name, list.map(toOsvAdvisory));
34247
+ }
34248
+ }
34249
+ if (sources.includes("gemnasium")) {
34250
+ const rows = await readRowsForPackages(advisoryFilePath(cacheDir, "gemnasium", ecosystem), wanted);
34251
+ for (const [name, list] of rows.entries()) {
34252
+ gemnasium.set(name, list.map(toGemnasiumAdvisory));
34253
+ }
34254
+ }
34255
+ return { osv, gemnasium };
34256
+ }
34257
+ function cacheEcosystemKey(ecosystem) {
34258
+ const def = getEcosystem(ecosystem);
34259
+ return def ? def.osvEcosystem : ecosystem;
34260
+ }
34261
+ function toOsvAdvisory(row) {
34262
+ return {
34263
+ advisoryId: row.advisoryId,
34264
+ aliases: row.aliases,
34265
+ ranges: row.ranges,
34266
+ versions: row.versions,
34267
+ severity: row.severity,
34268
+ summary: row.summary,
34269
+ url: row.url,
34270
+ malicious: row.malicious
34271
+ };
34272
+ }
34273
+ function toGemnasiumAdvisory(row) {
34274
+ return {
34275
+ advisoryId: row.advisoryId,
34276
+ aliases: row.aliases,
34277
+ ranges: row.ranges,
34278
+ versions: row.versions,
34279
+ severity: row.severity,
34280
+ summary: row.summary,
34281
+ url: row.url
34282
+ };
34283
+ }
34201
34284
 
34202
34285
  // src/cache/sync.ts
34203
34286
  async function planSync(options) {
@@ -34388,7 +34471,7 @@ async function seedGemnasium(options, meta3, item) {
34388
34471
  const writer = createRowWriter(path);
34389
34472
  let rowCount;
34390
34473
  try {
34391
- for await (const batch of streamGemnasiumArchive(progressFor(options, item), fetchOptionsFor(options, item))) {
34474
+ for await (const batch of streamGemnasiumArchive(headSha, progressFor(options, item), fetchOptionsFor(options, item))) {
34392
34475
  const rows = batch.rows.filter(function forEcosystem(row) {
34393
34476
  return row.ecosystem === item.ecosystem;
34394
34477
  });
@@ -34847,6 +34930,13 @@ function renderJson(summary, options, generatedAt) {
34847
34930
  findings: summary.findings
34848
34931
  }, null, 2) + "\n";
34849
34932
  }
34933
+ function hasUnavailableSource(summary) {
34934
+ return summary.projects.some(function projectLostASource(project) {
34935
+ return project.unauditable.some(function lost(entry) {
34936
+ return isSourceUnavailableReason(entry.reasonCode);
34937
+ });
34938
+ });
34939
+ }
34850
34940
  function shouldFail(summary, failOn) {
34851
34941
  if (failOn === "none") return false;
34852
34942
  if (failOn === "any") return summary.totalFindings > 0;
@@ -34994,7 +35084,7 @@ var SEVERITY_ORDER = ["critical", "high", "moderate", "low", "info"];
34994
35084
  function formatBytes(bytes, estimated = false) {
34995
35085
  if (bytes === null) return "unknown size";
34996
35086
  const prefix = estimated ? "~" : "";
34997
- if (bytes < 1024) return prefix + bytes + " B";
35087
+ if (bytes < 1024) return prefix + Math.round(bytes) + " B";
34998
35088
  const mib = bytes / (1024 * 1024);
34999
35089
  if (mib < 1) return prefix + (bytes / 1024).toFixed(0) + " KB";
35000
35090
  if (mib < 1024) return prefix + mib.toFixed(1) + " MB";
@@ -35083,9 +35173,34 @@ function createUi(options) {
35083
35173
  rl.close();
35084
35174
  }
35085
35175
  }
35086
- function seedDeclined() {
35176
+ function sourcesSwitchedOff(sources) {
35177
+ const names = sources.map(sourceLabel).join(", ");
35178
+ write(" " + c.dim + "\xB7 " + names + " switched off and never seeded \u2014 not scanned" + c.reset);
35179
+ }
35180
+ async function confirmRetry(failed) {
35181
+ if (!interactive || !process.stdin.isTTY || !process.stderr.isTTY) return false;
35182
+ const names = failed.map(function label(outcome) {
35183
+ return sourceLabel(outcome.source);
35184
+ }).join(", ");
35185
+ write("");
35186
+ write(" " + c.yellow + names + " could not be downloaded." + c.reset);
35187
+ write(" " + c.dim + "The scan can continue without it, but anything only that source knows about" + c.reset);
35188
+ write(" " + c.dim + "will be missing from the report." + c.reset);
35189
+ const rl = promises$1.createInterface({ input: process.stdin, output: process.stderr });
35190
+ try {
35191
+ const answer = await rl.question(" Try the download again? " + c.dim + "[Y/n]" + c.reset + " ");
35192
+ const normalized = answer.trim().toLowerCase();
35193
+ return normalized === "" || normalized === "y" || normalized === "yes";
35194
+ } finally {
35195
+ rl.close();
35196
+ }
35197
+ }
35198
+ function seedDeclined(gated) {
35087
35199
  write("");
35088
35200
  write(" " + c.dim + "Skipped. Run again and accept, or pass --yes, to enable the advisory sources." + c.reset);
35201
+ if (gated) {
35202
+ write(" " + c.yellow + "--fail-on cannot be evaluated without them; exiting non-zero rather than reporting clean." + c.reset);
35203
+ }
35089
35204
  write("");
35090
35205
  }
35091
35206
  function offlineNotice() {
@@ -35177,9 +35292,25 @@ function createUi(options) {
35177
35292
  }
35178
35293
  write(line);
35179
35294
  }
35295
+ function lostSourceLines(runSummary) {
35296
+ const seen = /* @__PURE__ */ new Map();
35297
+ for (const project of runSummary.projects) {
35298
+ for (const entry of project.unauditable) {
35299
+ if (!isSourceUnavailableReason(entry.reasonCode)) continue;
35300
+ if (seen.has(entry.scanner)) continue;
35301
+ seen.set(entry.scanner, entry.errorText || entry.reasonCode);
35302
+ }
35303
+ }
35304
+ return Array.from(seen, function line([scanner, why]) {
35305
+ return sourceLabel(scanner) + c.dim + " " + why + c.reset;
35306
+ });
35307
+ }
35180
35308
  function summary(runSummary, destination) {
35181
35309
  write("");
35182
- if (runSummary.totalFindings === 0) {
35310
+ const lost = lostSourceLines(runSummary);
35311
+ if (runSummary.totalFindings === 0 && lost.length > 0) {
35312
+ write(" " + c.yellow + c.bold + "No findings \u2014 but not everything could be checked." + c.reset);
35313
+ } else if (runSummary.totalFindings === 0) {
35183
35314
  write(" " + c.green + c.bold + "No findings." + c.reset + c.dim + " " + runSummary.projects.length + " project" + (runSummary.projects.length === 1 ? "" : "s") + " clean." + c.reset);
35184
35315
  } else {
35185
35316
  const parts = [];
@@ -35200,6 +35331,13 @@ function createUi(options) {
35200
35331
  write(" " + project.relPath.padEnd(38).slice(0, 38) + " " + breakdown);
35201
35332
  }
35202
35333
  }
35334
+ if (lost.length > 0) {
35335
+ write("");
35336
+ for (const line of lost) write(" " + c.yellow + "!" + c.reset + " " + line);
35337
+ if (options.failOn !== "none") {
35338
+ write(" " + c.dim + "--fail-on cannot be honoured by a scan that lost a source; exiting non-zero." + c.reset);
35339
+ }
35340
+ }
35203
35341
  write("");
35204
35342
  if (destination) {
35205
35343
  write(" " + c.bold + "Advisory written to" + c.reset + " " + destination);
@@ -35220,6 +35358,8 @@ function createUi(options) {
35220
35358
  discovered,
35221
35359
  noProjects,
35222
35360
  confirmSeed,
35361
+ confirmRetry,
35362
+ sourcesSwitchedOff,
35223
35363
  seedDeclined,
35224
35364
  offlineNotice,
35225
35365
  syncStatus,
@@ -35236,8 +35376,8 @@ function createUi(options) {
35236
35376
 
35237
35377
  // src/help.ts
35238
35378
  var CLI_VERSION = (function resolveVersion() {
35239
- if ("3.0.0".length > 0) {
35240
- return "3.0.0";
35379
+ if ("3.1.0".length > 0) {
35380
+ return "3.1.0";
35241
35381
  }
35242
35382
  const fromEnv = process.env.SENTINELLO_VERSION;
35243
35383
  if (fromEnv && fromEnv.trim().length > 0) return fromEnv.trim();
@@ -35270,10 +35410,10 @@ SOURCES
35270
35410
  --offline Skip the freshness check and use the cached data as-is.
35271
35411
  --cache-dir <path> Where the advisory cache lives.
35272
35412
  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.
35413
+ --feed-wait <secs> How long to keep retrying a feed that declines a download.
35414
+ Default: 15 \u2014 short, because a declined download is offered
35415
+ as a retry rather than waited out. 0 gives up on the first
35416
+ refusal, leaving that source unseeded.
35277
35417
 
35278
35418
  OUTPUT
35279
35419
  --out <file|-> Write the advisory to a file, or "-" for stdout. With "-" the
@@ -35290,7 +35430,10 @@ OUTPUT
35290
35430
  BEHAVIOUR
35291
35431
  --fail-on <level> Exit 2 when a finding at this severity or above exists.
35292
35432
  A severity, "any", or "none". Default: none.
35293
- -y, --yes Accept every prompt, including the first-run download.
35433
+ -y, --yes Accept every prompt, including the first-run download. Required on a
35434
+ non-TTY: without it the download is refused, and under --fail-on
35435
+ that refusal exits 1 rather than reporting a clean scan. npx has a
35436
+ separate prompt of its own: in CI use npx --yes sentinello --yes
35294
35437
  --doctor Print cache status, resolved settings, and detected projects.
35295
35438
  -q, --quiet Suppress the human-readable output.
35296
35439
  --verbose List every directory skipped by an ignore rule.
@@ -35300,7 +35443,8 @@ BEHAVIOUR
35300
35443
 
35301
35444
  EXIT CODES
35302
35445
  0 completed (findings alone are not a failure)
35303
- 1 a scan or configuration error
35446
+ 1 a scan or configuration error, or a --fail-on run that lost an advisory source
35447
+ (a gate cannot be honoured by a scan that could not consult its sources)
35304
35448
  2 the --fail-on threshold was met
35305
35449
 
35306
35450
  CONFIG
@@ -35446,35 +35590,50 @@ async function runScan(options, cacheDir, ui) {
35446
35590
  ui.noProjects(skipped);
35447
35591
  return EXIT_OK;
35448
35592
  }
35449
- if (!options.offline && options.sources.length > 0) {
35450
- const plan = await planSync({ cacheDir, sources: options.sources, ecosystem: DEFAULT_ECOSYSTEM });
35593
+ const sources = enabledSources(options.sources, await readCacheMeta(cacheDir));
35594
+ const switchedOff = options.sources.filter(function isOff(source) {
35595
+ return !sources.includes(source);
35596
+ });
35597
+ if (switchedOff.length > 0) ui.sourcesSwitchedOff(switchedOff);
35598
+ if (!options.offline && sources.length > 0) {
35599
+ const plan = await planSync({ cacheDir, sources, ecosystem: DEFAULT_ECOSYSTEM });
35451
35600
  if (plan.needsConsent && !options.assumeYes) {
35452
35601
  const approved = await ui.confirmSeed(plan);
35453
35602
  if (!approved) {
35454
- ui.seedDeclined();
35455
- return EXIT_OK;
35603
+ const gated = options.failOn !== "none";
35604
+ ui.seedDeclined(gated);
35605
+ return gated ? EXIT_ERROR : EXIT_OK;
35456
35606
  }
35457
35607
  }
35458
- const outcomes = await runSync({
35608
+ const syncOptions = {
35459
35609
  cacheDir,
35460
- sources: options.sources,
35610
+ sources,
35461
35611
  ecosystem: DEFAULT_ECOSYSTEM,
35462
35612
  retryWaitMs: retryWaitMsFor(options.feedWaitSeconds),
35463
35613
  onProgress: ui.syncProgress,
35464
35614
  onRetry: ui.syncRetry,
35465
35615
  onStatus: ui.syncStatus
35466
- }, plan);
35616
+ };
35617
+ let outcomes = await runSync(syncOptions, plan);
35467
35618
  ui.syncDone(outcomes);
35619
+ for (; ; ) {
35620
+ const failed = failedItems(plan, outcomes);
35621
+ if (failed.length === 0) break;
35622
+ const approved = await ui.confirmRetry(outcomes.filter(isErrorOutcome));
35623
+ if (!approved) break;
35624
+ outcomes = await runSync(syncOptions, { items: failed, seedBytes: 0, needsConsent: false });
35625
+ ui.syncDone(outcomes);
35626
+ }
35468
35627
  } else if (options.offline) {
35469
35628
  ui.offlineNotice();
35470
35629
  }
35471
35630
  const resolved = await resolveProjects(projects);
35472
35631
  const packageNames = collectPackageNames(resolved);
35473
- const cache = await loadCacheForPackages(cacheDir, DEFAULT_ECOSYSTEM, packageNames, options.sources);
35632
+ const cache = await loadCacheForPackages(cacheDir, DEFAULT_ECOSYSTEM, packageNames, sources);
35474
35633
  const meta3 = await readCacheMeta(cacheDir);
35475
35634
  const setup = {
35476
35635
  cacheDir,
35477
- sources: options.sources,
35636
+ sources,
35478
35637
  ecosystem: DEFAULT_ECOSYSTEM,
35479
35638
  includeNpmAudit: options.includeNpmAudit,
35480
35639
  seeded: {
@@ -35508,8 +35667,33 @@ async function runScan(options, cacheDir, ui) {
35508
35667
  }
35509
35668
  ui.summary(summary, destination);
35510
35669
  if (shouldFail(summary, options.failOn)) return EXIT_THRESHOLD;
35670
+ if (options.failOn !== "none" && hasUnavailableSource(summary)) return EXIT_ERROR;
35511
35671
  return EXIT_OK;
35512
35672
  }
35673
+ function feedSwitchedOff(source) {
35674
+ if (source === "osv") return osvFeedDisabled();
35675
+ return gemnasiumFeedDisabled();
35676
+ }
35677
+ function normalizerVersionFor(source) {
35678
+ if (source === "osv") return OSV_NORMALIZER_VERSION;
35679
+ return GEMNASIUM_NORMALIZER_VERSION;
35680
+ }
35681
+ function enabledSources(sources, meta3) {
35682
+ return sources.filter(function isUsable(source) {
35683
+ if (!feedSwitchedOff(source)) return true;
35684
+ return isSeeded(meta3, source, DEFAULT_ECOSYSTEM, normalizerVersionFor(source));
35685
+ });
35686
+ }
35687
+ function isErrorOutcome(outcome) {
35688
+ return outcome.status === "error";
35689
+ }
35690
+ function failedItems(plan, outcomes) {
35691
+ return plan.items.filter(function wasError(item) {
35692
+ return outcomes.some(function matches(outcome) {
35693
+ return outcome.status === "error" && outcome.source === item.source && outcome.ecosystem === item.ecosystem;
35694
+ });
35695
+ });
35696
+ }
35513
35697
  function retryWaitMsFor(feedWaitSeconds) {
35514
35698
  if (feedWaitSeconds === null) return void 0;
35515
35699
  return feedWaitSeconds * 1e3;