@xagents_in/unmask 0.1.3 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +86 -4
- package/dist/cli/annotations.mjs +145 -0
- package/dist/cli/audit.mjs +223 -0
- package/dist/cli/cache.mjs +488 -0
- package/dist/cli/compare.mjs +261 -0
- package/dist/cli/config.mjs +141 -0
- package/dist/cli/discovery.mjs +372 -0
- package/dist/cli/doctor.mjs +354 -0
- package/dist/cli/exceptions.mjs +170 -0
- package/dist/cli/file.mjs +271 -0
- package/dist/cli/files.mjs +141 -0
- package/dist/cli/history.mjs +232 -0
- package/dist/cli/hosted.mjs +251 -43
- package/dist/cli/info.mjs +125 -0
- package/dist/cli/init.mjs +57 -0
- package/dist/cli/install-guard.mjs +896 -0
- package/dist/cli/lockfile.mjs +330 -0
- package/dist/cli/main.mjs +97 -14
- package/dist/cli/preflight.mjs +388 -0
- package/dist/cli/sarif.mjs +100 -0
- package/dist/cli/shell-integration.mjs +122 -0
- package/dist/cli/terminal.mjs +396 -0
- package/dist/cli/watch.mjs +233 -0
- package/dist/cli/why.mjs +256 -0
- package/dist/core/advisories.mjs +101 -24
- package/dist/core/advisory-cache.mjs +295 -0
- package/dist/core/behavior-signals.mjs +1088 -0
- package/dist/core/dependency-graph.mjs +332 -0
- package/dist/core/evidence-exports.mjs +469 -0
- package/dist/core/guard-config.mjs +463 -0
- package/dist/core/guard-policy.mjs +308 -0
- package/dist/core/package-details.mjs +42 -6
- package/dist/core/report.mjs +7 -0
- package/dist/core/sarif.mjs +556 -0
- package/dist/core/syntax.mjs +296 -15
- package/dist/model/client.mjs +204 -56
- package/dist/model/diff-evidence.mjs +177 -0
- package/dist/model/summary.mjs +183 -0
- package/dist/server/advisory-result.mjs +137 -0
- package/dist/server/app.mjs +60 -2
- package/dist/server/dependency-screening.mjs +386 -0
- package/dist/server/exports.mjs +63 -0
- package/dist/server/guard-decision.mjs +37 -0
- package/dist/server/main.mjs +25 -7
- package/dist/server/queue.mjs +95 -53
- package/dist/server/store.mjs +21 -1
- package/dist/version.json +1 -1
- package/dist/viewer/render.mjs +10 -2
- package/dist/viewer/shell.mjs +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
import {
|
|
2
|
+
LockfileError,
|
|
3
|
+
explainDependencyPaths,
|
|
4
|
+
explainLockfileLocation,
|
|
5
|
+
parseNpmLockfile,
|
|
6
|
+
} from "../core/dependency-graph.mjs";
|
|
7
|
+
|
|
8
|
+
export const SCREENING_LIMITS = Object.freeze({
|
|
9
|
+
bodyBytes: 2 * 1024 * 1024,
|
|
10
|
+
bodyTimeoutMs: 10000,
|
|
11
|
+
identities: 200,
|
|
12
|
+
concurrency: 4,
|
|
13
|
+
deadlineMs: 60000,
|
|
14
|
+
advisoriesPerDependency: 20,
|
|
15
|
+
explanations: 50,
|
|
16
|
+
omissionExplanations: 50,
|
|
17
|
+
admissions: 6,
|
|
18
|
+
attempts: 30,
|
|
19
|
+
readers: 2,
|
|
20
|
+
admissionWindowMs: 600000,
|
|
21
|
+
});
|
|
22
|
+
const STATUSES = new Set([
|
|
23
|
+
"found",
|
|
24
|
+
"none-reported",
|
|
25
|
+
"incomplete",
|
|
26
|
+
"unavailable",
|
|
27
|
+
]);
|
|
28
|
+
const systemClock = { now: Date.now, setTimeout, clearTimeout };
|
|
29
|
+
const STATEMENT =
|
|
30
|
+
"Coverage describes exact registry identities only; the project root, local, rejected and unscreened entries were not checked. No reported advisory is not a safety verdict.";
|
|
31
|
+
|
|
32
|
+
// Keeps only fields the guard classifier needs, with affected severity limited to the screened package.
|
|
33
|
+
function advisorySummary(item, name) {
|
|
34
|
+
const affected = (Array.isArray(item.affected) ? item.affected : [])
|
|
35
|
+
.filter(
|
|
36
|
+
(entry) =>
|
|
37
|
+
entry?.package?.ecosystem === "npm" &&
|
|
38
|
+
entry.package.name === name &&
|
|
39
|
+
typeof entry.ecosystemSeverity === "string",
|
|
40
|
+
)
|
|
41
|
+
.slice(0, 8)
|
|
42
|
+
.map((entry) => ({
|
|
43
|
+
package: { ecosystem: "npm", name },
|
|
44
|
+
ecosystemSeverity: entry.ecosystemSeverity,
|
|
45
|
+
}));
|
|
46
|
+
return {
|
|
47
|
+
id: item.id,
|
|
48
|
+
aliases: Array.isArray(item.aliases) ? item.aliases.slice(0, 16) : [],
|
|
49
|
+
...(typeof item.summary === "string" ? { summary: item.summary } : {}),
|
|
50
|
+
...(typeof item.databaseSeverity === "string"
|
|
51
|
+
? { databaseSeverity: item.databaseSeverity }
|
|
52
|
+
: {}),
|
|
53
|
+
...(typeof item.withdrawn === "string"
|
|
54
|
+
? { withdrawn: item.withdrawn }
|
|
55
|
+
: {}),
|
|
56
|
+
...(affected.length ? { affected } : {}),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const unscreened = (identity, status, reason) => ({
|
|
61
|
+
...identity,
|
|
62
|
+
status,
|
|
63
|
+
complete: false,
|
|
64
|
+
reasons: [reason],
|
|
65
|
+
advisories: [],
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// Accepts a lookup only when it is bound to the exact identity and its status agrees with its advisories and completeness.
|
|
69
|
+
function normalize(identity, lookup, limits) {
|
|
70
|
+
if (
|
|
71
|
+
!lookup ||
|
|
72
|
+
typeof lookup !== "object" ||
|
|
73
|
+
!STATUSES.has(lookup.status) ||
|
|
74
|
+
!Array.isArray(lookup.advisories)
|
|
75
|
+
)
|
|
76
|
+
return unscreened(identity, "unavailable", "invalid-lookup-result");
|
|
77
|
+
if (
|
|
78
|
+
lookup.package?.name !== identity.name ||
|
|
79
|
+
lookup.package?.version !== identity.version
|
|
80
|
+
)
|
|
81
|
+
return unscreened(identity, "unavailable", "identity-mismatch");
|
|
82
|
+
const advisories = lookup.advisories
|
|
83
|
+
.filter((item) => item && typeof item.id === "string")
|
|
84
|
+
.map((item) => advisorySummary(item, identity.name));
|
|
85
|
+
const reasons = Array.isArray(lookup.reasons)
|
|
86
|
+
? lookup.reasons.filter((reason) => typeof reason === "string")
|
|
87
|
+
: [];
|
|
88
|
+
let status = lookup.status;
|
|
89
|
+
const settled =
|
|
90
|
+
lookup.complete === true &&
|
|
91
|
+
reasons.length === 0 &&
|
|
92
|
+
advisories.length === lookup.advisories.length;
|
|
93
|
+
if (
|
|
94
|
+
(status === "found" && (!advisories.length || !settled)) ||
|
|
95
|
+
(status === "none-reported" && (lookup.advisories.length > 0 || !settled))
|
|
96
|
+
) {
|
|
97
|
+
reasons.push("inconsistent-lookup-result");
|
|
98
|
+
status = advisories.length ? "incomplete" : "unavailable";
|
|
99
|
+
}
|
|
100
|
+
if (advisories.length > limits.advisoriesPerDependency)
|
|
101
|
+
reasons.push("advisory-output-limit");
|
|
102
|
+
return {
|
|
103
|
+
...identity,
|
|
104
|
+
status,
|
|
105
|
+
complete:
|
|
106
|
+
["found", "none-reported"].includes(status) && reasons.length === 0,
|
|
107
|
+
reasons,
|
|
108
|
+
advisories: advisories.slice(0, limits.advisoriesPerDependency),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Screens unique exact registry identities with bounded concurrency, count and a deadline that also bounds in-flight lookups.
|
|
113
|
+
export async function screenDependencyGraph(
|
|
114
|
+
graph,
|
|
115
|
+
{ lookup, limits: overrides = {}, clock = systemClock, signal } = {},
|
|
116
|
+
) {
|
|
117
|
+
const limits = { ...SCREENING_LIMITS, ...overrides };
|
|
118
|
+
const started = clock.now();
|
|
119
|
+
const results = new Array(graph.identities.length);
|
|
120
|
+
const queue = graph.identities
|
|
121
|
+
.slice(0, limits.identities)
|
|
122
|
+
.map((identity, i) => [i, identity]);
|
|
123
|
+
graph.identities.slice(limits.identities).forEach((identity, offset) => {
|
|
124
|
+
results[limits.identities + offset] = unscreened(
|
|
125
|
+
identity,
|
|
126
|
+
"not-screened",
|
|
127
|
+
"identity-limit",
|
|
128
|
+
);
|
|
129
|
+
});
|
|
130
|
+
const deadline = new AbortController();
|
|
131
|
+
const combined = signal
|
|
132
|
+
? AbortSignal.any([signal, deadline.signal])
|
|
133
|
+
: deadline.signal;
|
|
134
|
+
const timer = (clock.setTimeout ?? setTimeout)(
|
|
135
|
+
() => deadline.abort(),
|
|
136
|
+
limits.deadlineMs,
|
|
137
|
+
);
|
|
138
|
+
let stop;
|
|
139
|
+
const stopped = new Promise((resolve) => {
|
|
140
|
+
stop = () => resolve({ stopped: true });
|
|
141
|
+
combined.addEventListener("abort", stop, { once: true });
|
|
142
|
+
});
|
|
143
|
+
if (combined.aborted) stop();
|
|
144
|
+
const reason = () => (signal?.aborted ? "aborted" : "time-limit");
|
|
145
|
+
async function worker() {
|
|
146
|
+
for (let next = queue.shift(); next; next = queue.shift()) {
|
|
147
|
+
const [index, identity] = next;
|
|
148
|
+
if (combined.aborted || clock.now() - started >= limits.deadlineMs) {
|
|
149
|
+
results[index] = unscreened(identity, "not-screened", reason());
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
const pending = Promise.resolve()
|
|
153
|
+
.then(() => lookup(identity.spec, { signal: combined }))
|
|
154
|
+
.then(
|
|
155
|
+
(value) => ({ value }),
|
|
156
|
+
() => ({ failed: true }),
|
|
157
|
+
);
|
|
158
|
+
const outcome = await Promise.race([pending, stopped]);
|
|
159
|
+
results[index] = outcome.stopped
|
|
160
|
+
? unscreened(identity, "unavailable", reason())
|
|
161
|
+
: outcome.failed
|
|
162
|
+
? unscreened(identity, "unavailable", "lookup-failed")
|
|
163
|
+
: normalize(identity, outcome.value, limits);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
try {
|
|
167
|
+
await Promise.all(
|
|
168
|
+
Array.from(
|
|
169
|
+
{ length: Math.min(limits.concurrency, queue.length) },
|
|
170
|
+
worker,
|
|
171
|
+
),
|
|
172
|
+
);
|
|
173
|
+
} finally {
|
|
174
|
+
(clock.clearTimeout ?? clearTimeout)(timer);
|
|
175
|
+
combined.removeEventListener("abort", stop);
|
|
176
|
+
}
|
|
177
|
+
let explained = 0;
|
|
178
|
+
for (const item of results) {
|
|
179
|
+
if (!item.advisories.length) continue;
|
|
180
|
+
item.explanation =
|
|
181
|
+
explained++ < limits.explanations
|
|
182
|
+
? explainDependencyPaths(graph, item.spec)
|
|
183
|
+
: {
|
|
184
|
+
spec: item.spec,
|
|
185
|
+
locations: graph.index.bySpec.get(item.spec)?.length ?? 0,
|
|
186
|
+
direct: null,
|
|
187
|
+
paths: [],
|
|
188
|
+
complete: false,
|
|
189
|
+
reasons: ["explanation-limit"],
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
const count = (status) =>
|
|
193
|
+
results.filter((item) => item.status === status).length;
|
|
194
|
+
const advisoryBearing = results
|
|
195
|
+
.filter((item) => item.advisories.length)
|
|
196
|
+
.map((item) => item.spec);
|
|
197
|
+
const summary = {
|
|
198
|
+
entries: graph.entries,
|
|
199
|
+
identities: results.length,
|
|
200
|
+
advisoryBearing: advisoryBearing.length,
|
|
201
|
+
found: count("found"),
|
|
202
|
+
noneReported: count("none-reported"),
|
|
203
|
+
incomplete: count("incomplete"),
|
|
204
|
+
unavailable: count("unavailable"),
|
|
205
|
+
notScreened: count("not-screened"),
|
|
206
|
+
integrityConflicting: results.filter(
|
|
207
|
+
(item) => item.integrityStatus === "conflicting",
|
|
208
|
+
).length,
|
|
209
|
+
rejected: graph.rejected.length,
|
|
210
|
+
local: graph.local.length,
|
|
211
|
+
};
|
|
212
|
+
const registryComplete =
|
|
213
|
+
graph.rejected.length === 0 &&
|
|
214
|
+
summary.integrityConflicting === 0 &&
|
|
215
|
+
results.every((item) => item.complete);
|
|
216
|
+
return {
|
|
217
|
+
schemaVersion: "1",
|
|
218
|
+
source: "OSV",
|
|
219
|
+
lockfileVersion: graph.lockfileVersion,
|
|
220
|
+
root: { ...graph.root, location: "", status: "not-screened" },
|
|
221
|
+
screenedAt: new Date(started).toISOString(),
|
|
222
|
+
status: advisoryBearing.length
|
|
223
|
+
? "advisories-found"
|
|
224
|
+
: registryComplete
|
|
225
|
+
? "none-reported"
|
|
226
|
+
: "incomplete",
|
|
227
|
+
coverage: {
|
|
228
|
+
scope: "exact-registry-identities",
|
|
229
|
+
registryComplete,
|
|
230
|
+
projectScreened: false,
|
|
231
|
+
},
|
|
232
|
+
summary,
|
|
233
|
+
advisoryBearing,
|
|
234
|
+
statement: STATEMENT,
|
|
235
|
+
dependencies: results,
|
|
236
|
+
rejected: graph.rejected.map((item, index) => ({
|
|
237
|
+
...item,
|
|
238
|
+
status: "rejected",
|
|
239
|
+
explanation:
|
|
240
|
+
index < limits.omissionExplanations
|
|
241
|
+
? explainLockfileLocation(graph, item.location)
|
|
242
|
+
: {
|
|
243
|
+
location: item.location,
|
|
244
|
+
locations: 1,
|
|
245
|
+
direct: null,
|
|
246
|
+
paths: [],
|
|
247
|
+
complete: false,
|
|
248
|
+
reasons: ["explanation-limit"],
|
|
249
|
+
},
|
|
250
|
+
})),
|
|
251
|
+
local: graph.local.map((item) => ({ ...item, status: "not-screened" })),
|
|
252
|
+
limits: {
|
|
253
|
+
identities: limits.identities,
|
|
254
|
+
concurrency: limits.concurrency,
|
|
255
|
+
deadlineMs: limits.deadlineMs,
|
|
256
|
+
},
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function readJsonBody(req, limits) {
|
|
261
|
+
return new Promise((resolve, reject) => {
|
|
262
|
+
const chunks = [];
|
|
263
|
+
let size = 0,
|
|
264
|
+
done = false;
|
|
265
|
+
const timer = setTimeout(() => finish(408), limits.bodyTimeoutMs);
|
|
266
|
+
timer.unref?.();
|
|
267
|
+
function finish(status) {
|
|
268
|
+
if (done) return;
|
|
269
|
+
done = true;
|
|
270
|
+
clearTimeout(timer);
|
|
271
|
+
req.removeAllListeners("data");
|
|
272
|
+
if (status) {
|
|
273
|
+
req.on("error", () => {});
|
|
274
|
+
req.resume();
|
|
275
|
+
reject(Object.assign(new Error("body rejected"), { status }));
|
|
276
|
+
} else resolve(Buffer.concat(chunks, size));
|
|
277
|
+
}
|
|
278
|
+
if (Number(req.headers["content-length"]) > limits.bodyBytes)
|
|
279
|
+
return finish(413);
|
|
280
|
+
req.on("data", (chunk) => {
|
|
281
|
+
size += chunk.length;
|
|
282
|
+
if (size > limits.bodyBytes) finish(413);
|
|
283
|
+
else chunks.push(chunk);
|
|
284
|
+
});
|
|
285
|
+
req.on("end", () => finish());
|
|
286
|
+
req.on("error", () => finish(400));
|
|
287
|
+
req.on("aborted", () => finish(400));
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export function createDependencyScreening({
|
|
292
|
+
lookup,
|
|
293
|
+
limits: overrides = {},
|
|
294
|
+
clock = systemClock,
|
|
295
|
+
} = {}) {
|
|
296
|
+
if (lookup !== undefined && typeof lookup !== "function")
|
|
297
|
+
throw new TypeError("advisoryLookup must be a function");
|
|
298
|
+
const limits = { ...SCREENING_LIMITS, ...overrides };
|
|
299
|
+
let active = false,
|
|
300
|
+
reading = 0,
|
|
301
|
+
admissions = [],
|
|
302
|
+
attempts = [];
|
|
303
|
+
const limited = (retryAfter) => [
|
|
304
|
+
429,
|
|
305
|
+
{ error: "screening-limit", retryAfter },
|
|
306
|
+
{ "retry-after": String(retryAfter) },
|
|
307
|
+
];
|
|
308
|
+
const wait = (list, max, now) =>
|
|
309
|
+
list.length >= max
|
|
310
|
+
? Math.max(
|
|
311
|
+
1,
|
|
312
|
+
Math.ceil((list[0] + limits.admissionWindowMs - now) / 1000),
|
|
313
|
+
)
|
|
314
|
+
: 0;
|
|
315
|
+
return async function handle(req, res, json) {
|
|
316
|
+
if (req.method !== "POST")
|
|
317
|
+
return json(405, { error: "method-not-allowed" }, { allow: "POST" });
|
|
318
|
+
if (
|
|
319
|
+
req.headers["sec-fetch-site"] === "cross-site" ||
|
|
320
|
+
(req.headers.origin &&
|
|
321
|
+
req.headers.origin !== `http://${req.headers.host}` &&
|
|
322
|
+
req.headers.origin !== `https://${req.headers.host}`)
|
|
323
|
+
)
|
|
324
|
+
return json(403, { error: "origin-rejected" });
|
|
325
|
+
if (
|
|
326
|
+
!/^application\/json(?:\s*;\s*charset=utf-8)?$/i.test(
|
|
327
|
+
req.headers["content-type"] || "",
|
|
328
|
+
)
|
|
329
|
+
)
|
|
330
|
+
return json(415, { error: "expected-json-lockfile" });
|
|
331
|
+
if (!lookup) return json(503, { error: "advisory-lookup-unavailable" });
|
|
332
|
+
let now = clock.now();
|
|
333
|
+
attempts = attempts.filter((at) => now - at < limits.admissionWindowMs);
|
|
334
|
+
// Every body attempt is counted and concurrent reads are capped before any screening slot is held.
|
|
335
|
+
const attemptWait = wait(attempts, limits.attempts, now);
|
|
336
|
+
if (attemptWait || reading >= limits.readers)
|
|
337
|
+
return json(...limited(attemptWait || 5));
|
|
338
|
+
attempts.push(now);
|
|
339
|
+
let body;
|
|
340
|
+
reading++;
|
|
341
|
+
try {
|
|
342
|
+
body = await readJsonBody(req, limits);
|
|
343
|
+
} catch (error) {
|
|
344
|
+
return json(error.status || 400, {
|
|
345
|
+
error: error.status === 413 ? "lockfile-too-large" : "body-unavailable",
|
|
346
|
+
});
|
|
347
|
+
} finally {
|
|
348
|
+
reading--;
|
|
349
|
+
}
|
|
350
|
+
let graph;
|
|
351
|
+
try {
|
|
352
|
+
graph = parseNpmLockfile(
|
|
353
|
+
new TextDecoder("utf-8", { fatal: true }).decode(body),
|
|
354
|
+
);
|
|
355
|
+
} catch (error) {
|
|
356
|
+
return json(400, {
|
|
357
|
+
error: "invalid-lockfile",
|
|
358
|
+
reason: error instanceof LockfileError ? error.reason : "invalid-utf8",
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
now = clock.now();
|
|
362
|
+
admissions = admissions.filter((at) => now - at < limits.admissionWindowMs);
|
|
363
|
+
if (active) return json(...limited(5));
|
|
364
|
+
const admissionWait = wait(admissions, limits.admissions, now);
|
|
365
|
+
if (admissionWait) return json(...limited(admissionWait));
|
|
366
|
+
active = true;
|
|
367
|
+
admissions.push(now);
|
|
368
|
+
const controller = new AbortController();
|
|
369
|
+
const abort = () => {
|
|
370
|
+
if (!res.writableEnded) controller.abort();
|
|
371
|
+
};
|
|
372
|
+
res.on("close", abort);
|
|
373
|
+
try {
|
|
374
|
+
const result = await screenDependencyGraph(graph, {
|
|
375
|
+
lookup,
|
|
376
|
+
limits,
|
|
377
|
+
clock,
|
|
378
|
+
signal: controller.signal,
|
|
379
|
+
});
|
|
380
|
+
return json(200, result);
|
|
381
|
+
} finally {
|
|
382
|
+
active = false;
|
|
383
|
+
res.removeListener("close", abort);
|
|
384
|
+
}
|
|
385
|
+
};
|
|
386
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildCycloneDxSbom,
|
|
3
|
+
buildEvidenceManifest,
|
|
4
|
+
CYCLONEDX_MEDIA_TYPE,
|
|
5
|
+
} from "../core/evidence-exports.mjs";
|
|
6
|
+
|
|
7
|
+
const EXPORTS = {
|
|
8
|
+
"cdx.json": {
|
|
9
|
+
type: `${CYCLONEDX_MEDIA_TYPE}; charset=utf-8`,
|
|
10
|
+
filename: "unmask-sbom.cdx.json",
|
|
11
|
+
build: (report, versionInfo) =>
|
|
12
|
+
buildCycloneDxSbom(report, { toolVersion: versionInfo.version }),
|
|
13
|
+
},
|
|
14
|
+
"evidence.json": {
|
|
15
|
+
type: "application/json; charset=utf-8",
|
|
16
|
+
filename: "unmask-evidence-manifest.json",
|
|
17
|
+
build: (report, versionInfo, job) =>
|
|
18
|
+
buildEvidenceManifest(report, {
|
|
19
|
+
reportId: job.id,
|
|
20
|
+
completedAt: job.finishedAt,
|
|
21
|
+
versionInfo,
|
|
22
|
+
}),
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export function matchExportPath(path) {
|
|
27
|
+
const match = /^\/r\/([a-f0-9]{32})\.([a-z]+\.json)$/.exec(path);
|
|
28
|
+
return match && Object.hasOwn(EXPORTS, match[2])
|
|
29
|
+
? { id: match[1], kind: match[2] }
|
|
30
|
+
: null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function serveExport({ path, queue, versionInfo, send }) {
|
|
34
|
+
const route = matchExportPath(path);
|
|
35
|
+
if (!route) return false;
|
|
36
|
+
const json = (status, value, headers = {}, type = "application/json") =>
|
|
37
|
+
send(status, JSON.stringify(value), `${type}; charset=utf-8`, headers);
|
|
38
|
+
const job = queue.get(route.id);
|
|
39
|
+
if (!job) {
|
|
40
|
+
json(404, { error: "report-unavailable" });
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
if (["queued", "running"].includes(job.status)) {
|
|
44
|
+
json(202, { status: job.status }, { "retry-after": "3" });
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
if (!job.report) {
|
|
48
|
+
json(409, { error: "report-not-produced", status: job.status });
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
const spec = EXPORTS[route.kind];
|
|
52
|
+
let body;
|
|
53
|
+
try {
|
|
54
|
+
body = spec.build(job.report, versionInfo, job);
|
|
55
|
+
} catch {
|
|
56
|
+
json(422, { error: "report-not-exportable" });
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
send(200, JSON.stringify(body), spec.type, {
|
|
60
|
+
"content-disposition": `attachment; filename="${spec.filename}"`,
|
|
61
|
+
});
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import {
|
|
2
|
+
evaluateGuardDecision,
|
|
3
|
+
getGuardProfile,
|
|
4
|
+
} from "../core/guard-policy.mjs";
|
|
5
|
+
|
|
6
|
+
export function guardDecisionResponse(job, { search = "" } = {}) {
|
|
7
|
+
const params = new URLSearchParams(search);
|
|
8
|
+
const profile = params.has("profile") ? params.get("profile") : "strict";
|
|
9
|
+
try {
|
|
10
|
+
if (params.size > 1 || [...params.keys()].some((key) => key !== "profile"))
|
|
11
|
+
throw new TypeError("Invalid query");
|
|
12
|
+
getGuardProfile(profile);
|
|
13
|
+
} catch {
|
|
14
|
+
return {
|
|
15
|
+
status: 400,
|
|
16
|
+
body: {
|
|
17
|
+
error: "invalid-guard-profile",
|
|
18
|
+
profiles: ["strict", "balanced", "audit"],
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
if (!job) return { status: 404, body: { error: "report-unavailable" } };
|
|
23
|
+
const pending = ["queued", "running"].includes(job.status);
|
|
24
|
+
return {
|
|
25
|
+
status: pending ? 202 : 200,
|
|
26
|
+
body: {
|
|
27
|
+
...evaluateGuardDecision(job, { profile }),
|
|
28
|
+
reportId: job.id,
|
|
29
|
+
checkStatus: job.status,
|
|
30
|
+
assessedAt:
|
|
31
|
+
job.finishedAt === undefined
|
|
32
|
+
? null
|
|
33
|
+
: new Date(job.finishedAt).toISOString(),
|
|
34
|
+
},
|
|
35
|
+
headers: pending ? { "retry-after": "3" } : {},
|
|
36
|
+
};
|
|
37
|
+
}
|
package/dist/server/main.mjs
CHANGED
|
@@ -48,6 +48,7 @@ export async function startServer({
|
|
|
48
48
|
versionInfo,
|
|
49
49
|
modelClient,
|
|
50
50
|
enrichJob,
|
|
51
|
+
advisoryCache,
|
|
51
52
|
} = {}) {
|
|
52
53
|
if (
|
|
53
54
|
typeof host !== "string" ||
|
|
@@ -63,12 +64,20 @@ export async function startServer({
|
|
|
63
64
|
: modelClient === undefined
|
|
64
65
|
? await modelClientFromEnv()
|
|
65
66
|
: modelClient;
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
67
|
+
if (
|
|
68
|
+
advisoryCache !== undefined &&
|
|
69
|
+
(typeof advisoryCache?.lookup !== "function" || enrichJob !== undefined)
|
|
70
|
+
)
|
|
71
|
+
throw new TypeError("advisoryCache needs a lookup and no enrichJob");
|
|
72
|
+
// One cache per server: every advisory consumer derived from enrichJob shares it.
|
|
73
|
+
const cache =
|
|
74
|
+
enrichJob === undefined
|
|
75
|
+
? (advisoryCache ??
|
|
76
|
+
(await import("../core/advisory-cache.mjs")).createAdvisoryCache({
|
|
77
|
+
exposeStats: process.env.UNMASK_ADVISORY_CACHE_STATS === "1",
|
|
78
|
+
}))
|
|
79
|
+
: undefined;
|
|
80
|
+
const enrichment = cache ? cache.lookup : (enrichJob ?? undefined);
|
|
72
81
|
if (enrichment !== undefined && typeof enrichment !== "function")
|
|
73
82
|
throw new TypeError("enrichJob must be a function or null");
|
|
74
83
|
const run = await createRunner({
|
|
@@ -97,6 +106,15 @@ export async function startServer({
|
|
|
97
106
|
enrichment(spec, {
|
|
98
107
|
signal: AbortSignal.any([signal, shutdown.signal]),
|
|
99
108
|
}),
|
|
109
|
+
advisoryLookup:
|
|
110
|
+
enrichment === undefined
|
|
111
|
+
? undefined
|
|
112
|
+
: (spec, { signal } = {}) =>
|
|
113
|
+
enrichment(spec, {
|
|
114
|
+
signal: signal
|
|
115
|
+
? AbortSignal.any([signal, shutdown.signal])
|
|
116
|
+
: shutdown.signal,
|
|
117
|
+
}),
|
|
100
118
|
versionInfo: {
|
|
101
119
|
...(versionInfo || (await loadVersionInfo())),
|
|
102
120
|
modelConfigDigest:
|
|
@@ -134,7 +152,7 @@ export async function startServer({
|
|
|
134
152
|
await close();
|
|
135
153
|
throw error;
|
|
136
154
|
}
|
|
137
|
-
return { server, close };
|
|
155
|
+
return { server, close, ...(cache ? { advisoryCache: cache } : {}) };
|
|
138
156
|
}
|
|
139
157
|
|
|
140
158
|
async function main() {
|