@xagents_in/unmask 0.1.1 → 0.1.2

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.
@@ -0,0 +1,457 @@
1
+ import { parsePackageSpec } from "./acquire.mjs";
2
+
3
+ const ENDPOINT = "https://api.osv.dev/v1/query";
4
+ export const ADVISORY_LIMITS = Object.freeze({
5
+ timeoutMs: 12000,
6
+ responseBytes: 1024 * 1024,
7
+ pages: 3,
8
+ advisories: 100,
9
+ });
10
+ const systemClock = { now: Date.now, setTimeout, clearTimeout };
11
+ class LookupFailure extends Error {
12
+ constructor(reason) {
13
+ super(reason);
14
+ this.reason = reason;
15
+ }
16
+ }
17
+ const fail = (reason) => {
18
+ throw new LookupFailure(reason);
19
+ };
20
+ const object = (value) =>
21
+ value !== null && typeof value === "object" && !Array.isArray(value);
22
+ function advisory(value, mark) {
23
+ if (
24
+ !object(value) ||
25
+ typeof value.id !== "string" ||
26
+ !/^[a-zA-Z0-9:_.-]{1,256}$/.test(value.id)
27
+ ) {
28
+ mark("invalid-record");
29
+ return null;
30
+ }
31
+ let truncated = false;
32
+ const recordMark = mark;
33
+ mark = (reason) => {
34
+ if (reason === "record-limit") truncated = true;
35
+ recordMark(reason);
36
+ };
37
+ function string(value, max, clip = false) {
38
+ if (value === undefined) return undefined;
39
+ if (typeof value !== "string") {
40
+ mark("invalid-record");
41
+ return undefined;
42
+ }
43
+ const bytes = Buffer.from(value);
44
+ if (bytes.length <= max) return value;
45
+ mark("record-limit");
46
+ if (!clip) return undefined;
47
+ for (let end = max; end >= Math.max(0, max - 3); end--) {
48
+ try {
49
+ return new TextDecoder("utf-8", { fatal: true }).decode(
50
+ bytes.subarray(0, end),
51
+ );
52
+ } catch {}
53
+ }
54
+ return "";
55
+ }
56
+ function list(value, max, convert) {
57
+ if (value === undefined) return [];
58
+ if (!Array.isArray(value)) {
59
+ mark("invalid-record");
60
+ return [];
61
+ }
62
+ if (value.length > max) mark("record-limit");
63
+ return value
64
+ .slice(0, max)
65
+ .map(convert)
66
+ .filter((item) => item !== null && item !== undefined);
67
+ }
68
+ function https(value) {
69
+ if (
70
+ typeof value !== "string" ||
71
+ value.length > 2048 ||
72
+ !value.startsWith("https://") ||
73
+ /[\s\x00-\x1f\x7f]/.test(value)
74
+ ) {
75
+ mark("invalid-record");
76
+ return undefined;
77
+ }
78
+ try {
79
+ const url = new URL(value);
80
+ if (url.protocol === "https:" && !url.username && !url.password)
81
+ return value;
82
+ } catch {}
83
+ mark("invalid-record");
84
+ return undefined;
85
+ }
86
+ function timestamp(value) {
87
+ if (value === undefined) return undefined;
88
+ if (
89
+ typeof value === "string" &&
90
+ value.length <= 64 &&
91
+ /^\d{4}-\d{2}-\d{2}T.+Z$/.test(value) &&
92
+ Number.isFinite(Date.parse(value))
93
+ )
94
+ return value;
95
+ mark("invalid-record");
96
+ return undefined;
97
+ }
98
+ function severity(value) {
99
+ return list(value, 8, (entry) => {
100
+ if (
101
+ !object(entry) ||
102
+ typeof entry.type !== "string" ||
103
+ typeof entry.score !== "string"
104
+ ) {
105
+ mark("invalid-record");
106
+ return null;
107
+ }
108
+ const type = string(entry.type, 32),
109
+ score = string(entry.score, 512);
110
+ if (type === undefined || score === undefined) return null;
111
+ const out = { type, score };
112
+ if (entry.source !== undefined) {
113
+ const source = https(entry.source);
114
+ if (source) out.source = source;
115
+ }
116
+ return out;
117
+ });
118
+ }
119
+ function range(value) {
120
+ if (!object(value) || typeof value.type !== "string") {
121
+ mark("invalid-record");
122
+ return null;
123
+ }
124
+ const type = string(value.type, 32);
125
+ if (type === undefined) return null;
126
+ const out = {
127
+ type,
128
+ events: list(value.events, 32, (event) => {
129
+ if (!object(event)) {
130
+ mark("invalid-record");
131
+ return null;
132
+ }
133
+ const keys = Object.keys(event);
134
+ if (
135
+ keys.length !== 1 ||
136
+ !["introduced", "fixed", "last_affected", "limit"].includes(
137
+ keys[0],
138
+ ) ||
139
+ typeof event[keys[0]] !== "string"
140
+ ) {
141
+ mark("invalid-record");
142
+ return null;
143
+ }
144
+ const version = string(event[keys[0]], 256);
145
+ return version === undefined ? null : { [keys[0]]: version };
146
+ }),
147
+ };
148
+ if (value.repo !== undefined) {
149
+ const repo = https(value.repo);
150
+ if (repo) out.repo = repo;
151
+ }
152
+ return out;
153
+ }
154
+ const out = {
155
+ id: value.id,
156
+ aliases: [
157
+ ...new Set(
158
+ list(value.aliases, 16, (alias) => {
159
+ if (
160
+ typeof alias !== "string" ||
161
+ !/^[a-zA-Z0-9:_.-]{1,256}$/.test(alias)
162
+ ) {
163
+ mark("invalid-record");
164
+ return null;
165
+ }
166
+ return alias;
167
+ }),
168
+ ),
169
+ ],
170
+ severitySource: "OSV-declaration",
171
+ severity: severity(value.severity),
172
+ references: list(value.references, 16, (reference) => {
173
+ if (!object(reference)) {
174
+ mark("invalid-record");
175
+ return null;
176
+ }
177
+ const url = https(reference.url);
178
+ if (!url) return null;
179
+ const type = string(reference.type, 32);
180
+ return { ...(type !== undefined ? { type } : {}), url };
181
+ }),
182
+ affected: list(value.affected, 8, (affected) => {
183
+ if (
184
+ !object(affected) ||
185
+ !object(affected.package) ||
186
+ typeof affected.package.name !== "string" ||
187
+ typeof affected.package.ecosystem !== "string"
188
+ ) {
189
+ mark("invalid-record");
190
+ return null;
191
+ }
192
+ const name = string(affected.package.name, 214),
193
+ ecosystem = string(affected.package.ecosystem, 64);
194
+ if (name === undefined || ecosystem === undefined) return null;
195
+ const pkg = { name, ecosystem };
196
+ if (affected.package.purl !== undefined) {
197
+ const purl = string(affected.package.purl, 1024);
198
+ if (purl !== undefined) pkg.purl = purl;
199
+ }
200
+ const entry = {
201
+ package: pkg,
202
+ ranges: list(affected.ranges, 8, range),
203
+ versions: list(affected.versions, 64, (version) =>
204
+ string(version, 256),
205
+ ),
206
+ severity: severity(affected.severity),
207
+ };
208
+ const declared = string(affected.ecosystem_specific?.severity, 64);
209
+ if (declared !== undefined) entry.ecosystemSeverity = declared;
210
+ return entry;
211
+ }),
212
+ };
213
+ for (const field of ["summary", "modified", "published", "withdrawn"]) {
214
+ const item =
215
+ field === "summary"
216
+ ? string(value[field], 2048, true)
217
+ : timestamp(value[field]);
218
+ if (item !== undefined) out[field] = item;
219
+ }
220
+ const declared = string(value.database_specific?.severity, 64);
221
+ if (declared !== undefined) out.databaseSeverity = declared;
222
+ if (truncated) out.truncated = true;
223
+ return out;
224
+ }
225
+
226
+ export async function lookupNpmAdvisories(
227
+ spec,
228
+ {
229
+ fetchImpl = globalThis.fetch,
230
+ clock = systemClock,
231
+ signal,
232
+ limits: overrides = {},
233
+ } = {},
234
+ ) {
235
+ const started = clock.now();
236
+ const result = {
237
+ schemaVersion: "1",
238
+ source: "OSV",
239
+ package: null,
240
+ queriedAt: new Date(started).toISOString(),
241
+ status: "unavailable",
242
+ complete: false,
243
+ reasons: [],
244
+ advisories: [],
245
+ pagesFetched: 0,
246
+ limits: { ...ADVISORY_LIMITS },
247
+ };
248
+ const reasons = new Set();
249
+ const records = new Map();
250
+ const mark = (reason) => reasons.add(reason);
251
+ function finish() {
252
+ result.advisories = [...records.values()].sort((a, b) =>
253
+ a.id < b.id ? -1 : a.id > b.id ? 1 : 0,
254
+ );
255
+ result.reasons = [...reasons].sort();
256
+ result.complete = !reasons.size;
257
+ result.status = reasons.size
258
+ ? result.pagesFetched || records.size
259
+ ? "incomplete"
260
+ : "unavailable"
261
+ : records.size
262
+ ? "found"
263
+ : "none-reported";
264
+ return result;
265
+ }
266
+ let claim;
267
+ try {
268
+ claim = parsePackageSpec(spec);
269
+ } catch {
270
+ mark("invalid-package-spec");
271
+ return finish();
272
+ }
273
+ result.package = { ...claim, ecosystem: "npm" };
274
+ const limits = { ...ADVISORY_LIMITS, ...overrides };
275
+ if (
276
+ Object.keys(limits).some(
277
+ (key) =>
278
+ !Object.hasOwn(ADVISORY_LIMITS, key) ||
279
+ !Number.isSafeInteger(limits[key]) ||
280
+ limits[key] < 1 ||
281
+ limits[key] > ADVISORY_LIMITS[key],
282
+ )
283
+ ) {
284
+ mark("invalid-limits");
285
+ return finish();
286
+ }
287
+ result.limits = limits;
288
+ if (signal?.aborted) {
289
+ mark("aborted");
290
+ return finish();
291
+ }
292
+ const controller = new AbortController();
293
+ const combined = signal
294
+ ? AbortSignal.any([signal, controller.signal])
295
+ : controller.signal;
296
+ let timedOut = false,
297
+ active = true;
298
+ const readers = new Set();
299
+ const timer = clock.setTimeout(() => {
300
+ timedOut = true;
301
+ controller.abort();
302
+ }, limits.timeoutMs);
303
+ let interrupt;
304
+ const interrupted = new Promise((_, reject) => {
305
+ interrupt = () => {
306
+ for (const reader of readers) reader.cancel().catch(() => {});
307
+ reject(new LookupFailure(timedOut ? "timeout" : "aborted"));
308
+ };
309
+ combined.addEventListener("abort", interrupt, { once: true });
310
+ });
311
+ function alive() {
312
+ if (!active || combined.aborted) fail(timedOut ? "timeout" : "aborted");
313
+ if (clock.now() - started >= limits.timeoutMs) {
314
+ timedOut = true;
315
+ controller.abort();
316
+ fail("timeout");
317
+ }
318
+ }
319
+ async function page(token) {
320
+ alive();
321
+ const response = await fetchImpl(ENDPOINT, {
322
+ method: "POST",
323
+ redirect: "manual",
324
+ credentials: "omit",
325
+ referrerPolicy: "no-referrer",
326
+ signal: combined,
327
+ headers: {
328
+ "content-type": "application/json",
329
+ accept: "application/json",
330
+ },
331
+ body: JSON.stringify({
332
+ package: { name: claim.name, ecosystem: "npm" },
333
+ version: claim.version,
334
+ ...(token ? { page_token: token } : {}),
335
+ }),
336
+ });
337
+ if (combined.aborted || !active) response.body?.cancel().catch(() => {});
338
+ alive();
339
+ if (
340
+ response.redirected ||
341
+ (response.url && response.url !== ENDPOINT) ||
342
+ (response.status >= 300 && response.status < 400)
343
+ ) {
344
+ response.body?.cancel().catch(() => {});
345
+ fail("redirect-rejected");
346
+ }
347
+ if (response.status !== 200) {
348
+ response.body?.cancel().catch(() => {});
349
+ fail("http-error");
350
+ }
351
+ if (
352
+ !/^application\/json(?:\s*;\s*charset=utf-8)?$/i.test(
353
+ response.headers.get("content-type") ?? "",
354
+ )
355
+ ) {
356
+ response.body?.cancel().catch(() => {});
357
+ fail("invalid-response");
358
+ }
359
+ const declared = response.headers.get("content-length");
360
+ if (
361
+ declared !== null &&
362
+ (!/^\d+$/.test(declared) || Number(declared) > limits.responseBytes)
363
+ ) {
364
+ response.body?.cancel().catch(() => {});
365
+ fail("response-limit");
366
+ }
367
+ const reader = response.body?.getReader();
368
+ if (!reader) fail("invalid-response");
369
+ readers.add(reader);
370
+ const chunks = [];
371
+ let size = 0;
372
+ try {
373
+ for (;;) {
374
+ alive();
375
+ const { done, value } = await reader.read();
376
+ alive();
377
+ if (done) break;
378
+ size += value.byteLength;
379
+ if (size > limits.responseBytes) fail("response-limit");
380
+ chunks.push(value);
381
+ }
382
+ } finally {
383
+ readers.delete(reader);
384
+ reader.cancel().catch(() => {});
385
+ reader.releaseLock();
386
+ }
387
+ let parsed;
388
+ try {
389
+ parsed = JSON.parse(
390
+ new TextDecoder("utf-8", { fatal: true }).decode(
391
+ Buffer.concat(chunks, size),
392
+ ),
393
+ );
394
+ } catch {
395
+ fail("invalid-response");
396
+ }
397
+ if (
398
+ !object(parsed) ||
399
+ Object.hasOwn(parsed, "error") ||
400
+ (parsed.vulns !== undefined && !Array.isArray(parsed.vulns)) ||
401
+ (parsed.vulns === undefined &&
402
+ parsed.next_page_token === undefined &&
403
+ Object.keys(parsed).length > 0)
404
+ )
405
+ fail("invalid-response");
406
+ if (
407
+ parsed.next_page_token !== undefined &&
408
+ (typeof parsed.next_page_token !== "string" ||
409
+ parsed.next_page_token.length > 4096 ||
410
+ /[\x00-\x1f\x7f]/.test(parsed.next_page_token))
411
+ )
412
+ fail("invalid-response");
413
+ return parsed;
414
+ }
415
+ async function query() {
416
+ const tokens = new Set();
417
+ let token;
418
+ for (let i = 0; i < limits.pages; i++) {
419
+ const value = await page(token);
420
+ alive();
421
+ result.pagesFetched++;
422
+ for (const raw of value.vulns ?? []) {
423
+ alive();
424
+ const item = advisory(raw, mark);
425
+ if (!item || records.has(item.id)) continue;
426
+ if (records.size >= limits.advisories) {
427
+ mark("advisory-limit");
428
+ return;
429
+ }
430
+ records.set(item.id, item);
431
+ }
432
+ token = value.next_page_token;
433
+ if (!token) return;
434
+ if (tokens.has(token)) {
435
+ mark("pagination-loop");
436
+ return;
437
+ }
438
+ tokens.add(token);
439
+ if (records.size >= limits.advisories) {
440
+ mark("advisory-limit");
441
+ return;
442
+ }
443
+ }
444
+ mark("page-limit");
445
+ }
446
+ try {
447
+ await Promise.race([query(), interrupted]);
448
+ } catch (error) {
449
+ mark(error instanceof LookupFailure ? error.reason : "request-failed");
450
+ } finally {
451
+ active = false;
452
+ clock.clearTimeout(timer);
453
+ combined.removeEventListener("abort", interrupt);
454
+ for (const reader of readers) reader.cancel().catch(() => {});
455
+ }
456
+ return finish();
457
+ }