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.
- package/README.md +7 -3
- package/dist/cli.cjs +561 -385
- 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
|
|
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
|
|
3730
|
+
var Readable = require_stream_readable();
|
|
3729
3731
|
var Writable = require_stream_writable();
|
|
3730
|
-
util.inherits(Duplex,
|
|
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
|
-
|
|
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 =
|
|
4033
|
+
module2.exports = Readable;
|
|
4032
4034
|
var isArray = require_isarray();
|
|
4033
4035
|
var Duplex;
|
|
4034
|
-
|
|
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(
|
|
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
|
|
4110
|
+
function Readable(options) {
|
|
4109
4111
|
Duplex = Duplex || require_stream_duplex();
|
|
4110
|
-
if (!(this instanceof
|
|
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(
|
|
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
|
-
|
|
4134
|
-
|
|
4135
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4218
|
+
Readable.prototype.isPaused = function() {
|
|
4217
4219
|
return this._readableState.flowing === false;
|
|
4218
4220
|
};
|
|
4219
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4352
|
+
Readable.prototype._read = function(n) {
|
|
4351
4353
|
this.emit("error", new Error("_read() is not implemented"));
|
|
4352
4354
|
};
|
|
4353
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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 =
|
|
33525
|
-
var SLOW_RETRY_WAITS_MS = [
|
|
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
|
|
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
|
|
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
|
-
|
|
33277
|
+
await discard(response);
|
|
33549
33278
|
if (options && options.abortSignal && options.abortSignal.aborted) break;
|
|
33550
33279
|
if (slow) {
|
|
33551
|
-
const
|
|
33552
|
-
if (
|
|
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 -
|
|
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
|
-
|
|
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
|
|
33613
|
-
|
|
33614
|
-
|
|
33615
|
-
|
|
33616
|
-
|
|
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
|
|
33398
|
+
const stream = response.stream;
|
|
33619
33399
|
if (onProgress) {
|
|
33620
33400
|
let bytesRead = 0;
|
|
33621
|
-
stream
|
|
33401
|
+
stream.on("data", function countChunk(chunk) {
|
|
33622
33402
|
bytesRead += chunk.length;
|
|
33623
33403
|
onProgress(bytesRead, contentLength);
|
|
33624
33404
|
});
|
|
33625
33405
|
}
|
|
33626
|
-
return { stream
|
|
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
|
-
|
|
33772
|
-
|
|
33773
|
-
|
|
33774
|
-
|
|
33775
|
-
|
|
33776
|
-
|
|
33777
|
-
entry.
|
|
33778
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
34172
|
-
|
|
34173
|
-
|
|
34174
|
-
|
|
34175
|
-
|
|
34176
|
-
|
|
34177
|
-
|
|
34178
|
-
entry.
|
|
34179
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
35240
|
-
return "3.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
|
|
35274
|
-
|
|
35275
|
-
|
|
35276
|
-
|
|
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
|
-
|
|
35450
|
-
|
|
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
|
-
|
|
35455
|
-
|
|
35595
|
+
const gated = options.failOn !== "none";
|
|
35596
|
+
ui.seedDeclined(gated);
|
|
35597
|
+
return gated ? EXIT_ERROR : EXIT_OK;
|
|
35456
35598
|
}
|
|
35457
35599
|
}
|
|
35458
|
-
const
|
|
35600
|
+
const syncOptions = {
|
|
35459
35601
|
cacheDir,
|
|
35460
|
-
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
|
-
}
|
|
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,
|
|
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
|
|
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;
|