@xagents_in/unmask 0.1.2 → 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.
Files changed (56) hide show
  1. package/README.md +301 -85
  2. package/dist/cli/annotations.mjs +145 -0
  3. package/dist/cli/audit.mjs +223 -0
  4. package/dist/cli/batch.mjs +218 -0
  5. package/dist/cli/cache.mjs +488 -0
  6. package/dist/cli/compare.mjs +261 -0
  7. package/dist/cli/config.mjs +141 -0
  8. package/dist/cli/discovery.mjs +372 -0
  9. package/dist/cli/doctor.mjs +354 -0
  10. package/dist/cli/exceptions.mjs +170 -0
  11. package/dist/cli/file.mjs +271 -0
  12. package/dist/cli/files.mjs +141 -0
  13. package/dist/cli/history.mjs +232 -0
  14. package/dist/cli/hosted.mjs +251 -43
  15. package/dist/cli/info.mjs +125 -0
  16. package/dist/cli/init.mjs +57 -0
  17. package/dist/cli/install-guard.mjs +896 -0
  18. package/dist/cli/lockfile.mjs +330 -0
  19. package/dist/cli/main.mjs +101 -14
  20. package/dist/cli/preflight.mjs +388 -0
  21. package/dist/cli/sarif.mjs +100 -0
  22. package/dist/cli/scan.mjs +128 -0
  23. package/dist/cli/shell-integration.mjs +122 -0
  24. package/dist/cli/terminal.mjs +396 -0
  25. package/dist/cli/watch.mjs +233 -0
  26. package/dist/cli/why.mjs +256 -0
  27. package/dist/core/advisories.mjs +101 -24
  28. package/dist/core/advisory-cache.mjs +295 -0
  29. package/dist/core/behavior-signals.mjs +1088 -0
  30. package/dist/core/dependency-graph.mjs +332 -0
  31. package/dist/core/evidence-exports.mjs +469 -0
  32. package/dist/core/guard-config.mjs +463 -0
  33. package/dist/core/guard-policy.mjs +308 -0
  34. package/dist/core/package-details.mjs +42 -6
  35. package/dist/core/registry-facts.mjs +342 -0
  36. package/dist/core/report-exploration.mjs +328 -0
  37. package/dist/core/report.mjs +7 -0
  38. package/dist/core/sarif.mjs +556 -0
  39. package/dist/core/syntax.mjs +296 -15
  40. package/dist/model/client.mjs +204 -56
  41. package/dist/model/diff-evidence.mjs +177 -0
  42. package/dist/model/summary.mjs +183 -0
  43. package/dist/server/advisory-result.mjs +137 -0
  44. package/dist/server/app.mjs +66 -2
  45. package/dist/server/dependency-screening.mjs +386 -0
  46. package/dist/server/exports.mjs +63 -0
  47. package/dist/server/guard-decision.mjs +37 -0
  48. package/dist/server/main.mjs +25 -7
  49. package/dist/server/queue.mjs +95 -53
  50. package/dist/server/registry-facts.mjs +118 -0
  51. package/dist/server/report-exploration.mjs +87 -0
  52. package/dist/server/store.mjs +21 -1
  53. package/dist/version.json +1 -1
  54. package/dist/viewer/render.mjs +10 -2
  55. package/dist/viewer/shell.mjs +1 -1
  56. package/package.json +1 -1
@@ -0,0 +1,183 @@
1
+ export const SUMMARY_POLICY = "every-check-2";
2
+ const SUMMARY_TEMPLATE = "≤600 chars";
3
+ const SUMMARY_TASK =
4
+ " Ground summary and assessment in scope and evidence; no safety verdicts.";
5
+ const ASSESSMENT_TEMPLATE = {
6
+ uncertainty: "unchecked ≤300",
7
+ nextAction: "review step ≤200",
8
+ };
9
+ const VERDICT =
10
+ /\b(?:(?:un)?safe(?:ly|ty)?|benign|harmless|malware-free|risk-free|trustworthy|no risks?)\b/iu;
11
+ // Only exact caveat templates may mention safety; any other negation still counts as a verdict.
12
+ const CAVEAT =
13
+ /\bnot (?:a |an )?(?:proof|evidence|guarantee|indication)(?: that)?(?: (?:the|this) package| it)?(?: is)? safe\b|\b(?:not a|no) safety (?:verdict|signal|claim|guarantee)\b|\bdoes not (?:mean|imply|prove|establish|show) (?:that )?(?:it|the package|this package) is safe\b/giu;
14
+ const ABSENCE_CLAIM =
15
+ /\b(?:absent|missing|lacks?|omits?)\b|does not (?:exist|contain|include)|not (?:present|in)\b|not found(?! within supplied evidence)/iu;
16
+ const SCOPED_ABSENCE =
17
+ /\b(?:absent|missing|not (?:present|found|in))\s+(?:(?:from|in|within)\s+)?(?:the\s+)?(?:selected source|supplied (?:evidence|scope))\b|\bsource(?: archive)? (?:is|was) (?:missing|absent)\b|\bomits?\s+\d+\s+(?:evidence\s+)?(?:items?|groups?)\b/giu;
18
+ const REVIEW_STEP =
19
+ /^(?:review|inspect|compare|confirm|verify|check|locate|examine|trace|investigate|read|diff|audit)\b/iu;
20
+ const GO_AHEAD =
21
+ /\b(?:proceed|go ahead|approve|ok(?:ay)? to|fine to|safe to|allow(?:ed)? to install)\b/iu;
22
+ // Controls, invisible format characters and line separators are rejected; newline and tab stay allowed.
23
+ const CONTROL = /(?![\n\t])[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/u;
24
+ const FENCE = /^\s*```(?:json)?[ \t]*\r?\n([\s\S]*?)\r?\n[ \t]*```\s*$/u;
25
+ export const hasControl = (value) => CONTROL.test(value);
26
+ // One whole-body code fence is unwrapped; JSON embedded in other prose is still rejected.
27
+ export const unfence = (raw) => FENCE.exec(raw)?.[1] ?? raw;
28
+ const counter = (value) =>
29
+ Number.isSafeInteger(value) && value >= 0 ? value : null;
30
+ const ruleIds = (items) =>
31
+ [
32
+ ...new Set(
33
+ items
34
+ .map((item) => item?.ruleId)
35
+ .filter(
36
+ (id) => typeof id === "string" && /^[a-z0-9-]{1,64}$/u.test(id),
37
+ ),
38
+ ),
39
+ ].sort();
40
+ const list = (value) => (Array.isArray(value) ? value : []);
41
+
42
+ export function scopeFor(report, bundle) {
43
+ const files = list(report.files);
44
+ const states = {};
45
+ for (const file of files) {
46
+ const state = /^[a-z-]{1,32}$/u.test(file?.state ?? "")
47
+ ? file.state
48
+ : "other";
49
+ states[state] = (states[state] ?? 0) + 1;
50
+ }
51
+ const coverage = report.coverage ?? {};
52
+ const findings = list(report.policy?.findings);
53
+ const cues = list(report.cues);
54
+ const supplied = new Set(list(bundle.evidenceGroups).map((g) => g.ruleId));
55
+ const scope = {
56
+ files: files.length,
57
+ states,
58
+ inventory: counter(coverage.inventory?.files),
59
+ complete: coverage.inventory?.complete === true,
60
+ compared: counter(coverage.compared?.files),
61
+ syntax: counter(coverage.syntaxInspected?.files),
62
+ source: /^[a-z-]{1,32}$/u.test(report.source?.status ?? "")
63
+ ? report.source.status
64
+ : "unknown",
65
+ exit: counter(report.policy?.exitCode),
66
+ findings: findings.length,
67
+ cues: cues.length,
68
+ // Rule IDs already named by a supplied group are not repeated.
69
+ unseenRules: ruleIds([...findings, ...cues])
70
+ .filter((id) => !supplied.has(id))
71
+ .slice(0, 6),
72
+ supplied: bundle.selected.size,
73
+ omitted: bundle.omittedEvidenceIds.length,
74
+ };
75
+ for (const [key, value] of Object.entries(scope))
76
+ if (value === null || (Array.isArray(value) && !value.length))
77
+ delete scope[key];
78
+ return scope;
79
+ }
80
+
81
+ export function summaryBundle(promptFor, report, inputLimit) {
82
+ let limit = inputLimit;
83
+ for (let pass = 0; pass < 6; pass++) {
84
+ const bundle = promptFor(report, limit);
85
+ const scope = scopeFor(report, bundle);
86
+ const payload = JSON.parse(bundle.prompt);
87
+ const prompt = JSON.stringify({
88
+ ...payload,
89
+ task: payload.task + SUMMARY_TASK,
90
+ output: {
91
+ summary: SUMMARY_TEMPLATE,
92
+ assessment: ASSESSMENT_TEMPLATE,
93
+ ...payload.output,
94
+ },
95
+ scope,
96
+ });
97
+ const over = Buffer.byteLength(prompt) + 256 - inputLimit;
98
+ if (over <= 0 || pass === 5) return { ...bundle, prompt, scope };
99
+ // Repack below the bytes actually used so each pass strictly shrinks evidence.
100
+ limit = Math.min(limit, Buffer.byteLength(bundle.prompt) + 256) - over;
101
+ }
102
+ }
103
+
104
+ function prose(value, max, secret) {
105
+ return (
106
+ typeof value === "string" &&
107
+ value.trim().length > 0 &&
108
+ value.length <= max &&
109
+ !value.includes(secret) &&
110
+ !hasControl(value) &&
111
+ !VERDICT.test(value.replace(CAVEAT, " ")) &&
112
+ !ABSENCE_CLAIM.test(value.replace(SCOPED_ABSENCE, " "))
113
+ );
114
+ }
115
+
116
+ const NONE = "(?:no|zero|0|without(?: any)?)\\s+";
117
+ const UNLOCALIZED = "(?!\\s+(?:in|for|on|within)\\b)";
118
+ const NO_FINDINGS = new RegExp(
119
+ `\\b${NONE}(?:default\\s+)?(?:policy\\s+)?findings?\\b${UNLOCALIZED}`,
120
+ "iu",
121
+ );
122
+ const NO_CUES = new RegExp(
123
+ `\\b${NONE}(?:review\\s+)?(?:cues?|suggestions?|observations?)\\b${UNLOCALIZED}`,
124
+ "iu",
125
+ );
126
+ const ALL_SAME = new RegExp(
127
+ `\\b(?:all|every|each)\\b[^.;]{0,40}?\\b(?:byte-identical|identical|unchanged|match(?:es|ed)?)\\b|\\b(?:no|zero)\\s+(?:file\\s+)?(?:changes?|differences?)\\b${UNLOCALIZED}|\\bno\\s+files?\\s+(?:changed|differ(?:s|ed)?)\\b|\\bnothing\\s+(?:changed|differs)\\b`,
128
+ "iu",
129
+ );
130
+ // Whole-report claims must not contradict the deterministic scope the model was given.
131
+ export function consistentWithScope(value, scope = {}) {
132
+ const states = scope.states ?? {};
133
+ const differs = Object.keys(states).some(
134
+ (state) => state !== "byte-identical" && states[state] > 0,
135
+ );
136
+ return !(
137
+ (scope.findings > 0 && NO_FINDINGS.test(value)) ||
138
+ (scope.cues > 0 && NO_CUES.test(value)) ||
139
+ (differs && ALL_SAME.test(value))
140
+ );
141
+ }
142
+
143
+ export function validSummary(value, secret, scope) {
144
+ return prose(value, 600, secret) && consistentWithScope(value, scope);
145
+ }
146
+
147
+ export function validAssessment(value, secret) {
148
+ return (
149
+ value !== null &&
150
+ typeof value === "object" &&
151
+ !Array.isArray(value) &&
152
+ Object.keys(value).length === 2 &&
153
+ prose(value.uncertainty, 300, secret) &&
154
+ prose(value.nextAction, 200, secret) &&
155
+ REVIEW_STEP.test(value.nextAction.trim()) &&
156
+ !GO_AHEAD.test(value.nextAction)
157
+ );
158
+ }
159
+
160
+ export function checkedScopeFor(bundle) {
161
+ const { scope } = bundle;
162
+ const notChecked = ["runtime-behavior"];
163
+ if (scope.omitted) notChecked.push("omitted-evidence");
164
+ if (!scope.complete) notChecked.push("incomplete-inventory");
165
+ if (!["selected", "resolved"].includes(scope.source))
166
+ notChecked.push("unselected-source");
167
+ for (const [key, code] of [
168
+ ["compared", "uncompared-files"],
169
+ ["syntax", "uninspected-syntax"],
170
+ ])
171
+ if (!(scope[key] >= (scope.inventory ?? 0))) notChecked.push(code);
172
+ return {
173
+ basis: "deterministic-prompt-selection",
174
+ evidenceSupplied: scope.supplied,
175
+ evidenceOmitted: scope.omitted,
176
+ groupsSupplied: bundle.evidenceGroups.length,
177
+ groupsOmitted: bundle.omittedEvidenceGroups.length,
178
+ paths: [...new Set([...bundle.selected.values()].map((e) => e.path))]
179
+ .sort()
180
+ .slice(0, 16),
181
+ notChecked,
182
+ };
183
+ }
@@ -0,0 +1,137 @@
1
+ import {
2
+ boundedVulnerabilityCheck,
3
+ unavailableVulnerabilityCheck,
4
+ } from "./enrichment.mjs";
5
+
6
+ const escape = (value) =>
7
+ String(value ?? "").replace(
8
+ /[&<>"']/g,
9
+ (ch) =>
10
+ ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[
11
+ ch
12
+ ],
13
+ );
14
+
15
+ const ASSESSMENTS = {
16
+ found: "advisories-reported",
17
+ incomplete: "advisory-lookup-incomplete",
18
+ "none-reported": "no-advisory-reported",
19
+ unavailable: "advisory-lookup-unavailable",
20
+ };
21
+
22
+ export function advisoryOnlyResult(vulnerabilityCheck, { artifactReason }) {
23
+ const status = vulnerabilityCheck?.status;
24
+ const assessment = ASSESSMENTS[status] ?? "advisory-lookup-unavailable";
25
+ return {
26
+ schemaVersion: "1",
27
+ kind: "advisory-only",
28
+ package: vulnerabilityCheck?.package ?? null,
29
+ assessment,
30
+ advisoryCount: vulnerabilityCheck?.advisories?.length ?? 0,
31
+ artifactVerification: { status: "not-performed", reason: artifactReason },
32
+ sourceComparison: { status: "not-performed", reason: artifactReason },
33
+ safetyVerdict: "not-provided",
34
+ notice:
35
+ assessment === "advisories-reported" ||
36
+ (assessment === "advisory-lookup-incomplete" &&
37
+ vulnerabilityCheck.advisories.length > 0)
38
+ ? "Published advisories match this exact version. The registry artifact was not inspected, so no tarball or source evidence is included."
39
+ : "No artifact was inspected. Absence of a current advisory, or an unavailable lookup, is not evidence of safety, and an unacquirable artifact is not by itself evidence of malware.",
40
+ vulnerabilityCheck,
41
+ };
42
+ }
43
+
44
+ export function renderAdvisoryResult(result) {
45
+ const check = result.vulnerabilityCheck;
46
+ const items = (check?.advisories ?? [])
47
+ .slice(0, 50)
48
+ .map(
49
+ (item) =>
50
+ `<li><code>${escape(item.id)}</code>${item.aliases?.length ? ` (${item.aliases.map(escape).join(", ")})` : ""}${item.summary ? `: ${escape(item.summary)}` : ""}</li>`,
51
+ )
52
+ .join("");
53
+ return `<section aria-label="Advisory-only result"><h2>Advisory-only result</h2><p class="status failed">${escape(result.assessment)}</p><p>${escape(result.notice)}</p>${items ? `<ul>${items}</ul>` : ""}<p class="meta">OSV lookup status: <code>${escape(check?.status)}</code>${check?.reasons?.length ? `; reasons: <code>${check.reasons.map(escape).join(", ")}</code>` : ""}. Artifact verification: <code>${escape(result.artifactVerification.status)}</code>.</p></section>`;
54
+ }
55
+
56
+ export function withAdvisoryResult(statusHtml, job) {
57
+ if (!job.advisoryResult) return statusHtml;
58
+ return statusHtml.replace(
59
+ "</div></main>",
60
+ () => renderAdvisoryResult(job.advisoryResult) + "</div></main>",
61
+ );
62
+ }
63
+
64
+ export const ADVISORY_ENDPOINT_LIMITS = Object.freeze({
65
+ maxLookups: 30,
66
+ windowMs: 600000,
67
+ maxActive: 2,
68
+ timeoutMs: 15000,
69
+ });
70
+
71
+ export function createAdvisoryService({
72
+ lookup,
73
+ clock,
74
+ limits = ADVISORY_ENDPOINT_LIMITS,
75
+ }) {
76
+ const inflight = new Map();
77
+ const shutdown = new AbortController();
78
+ let admissions = [];
79
+ function run(spec) {
80
+ const controller = new AbortController();
81
+ const signal = AbortSignal.any([shutdown.signal, controller.signal]);
82
+ let timer;
83
+ const timeout = new Promise((resolve) => {
84
+ timer = clock.setTimeout(() => {
85
+ controller.abort();
86
+ resolve(unavailableVulnerabilityCheck(spec, clock, "timeout"));
87
+ }, limits.timeoutMs);
88
+ timer?.unref?.();
89
+ });
90
+ return Promise.race([
91
+ Promise.resolve()
92
+ .then(() => lookup(spec, { signal }))
93
+ .then((value) => boundedVulnerabilityCheck(value, spec, clock))
94
+ .catch(() =>
95
+ unavailableVulnerabilityCheck(spec, clock, "lookup-failed"),
96
+ ),
97
+ timeout,
98
+ ])
99
+ .then((check) =>
100
+ advisoryOnlyResult(check, { artifactReason: "not-requested" }),
101
+ )
102
+ .finally(() => {
103
+ clock.clearTimeout(timer);
104
+ inflight.delete(spec);
105
+ });
106
+ }
107
+ return {
108
+ request(spec) {
109
+ const existing = inflight.get(spec);
110
+ if (existing) return { promise: existing, reused: true };
111
+ const now = clock.now();
112
+ admissions = admissions.filter((time) => now - time < limits.windowMs);
113
+ if (
114
+ shutdown.signal.aborted ||
115
+ inflight.size >= limits.maxActive ||
116
+ admissions.length >= limits.maxLookups
117
+ )
118
+ return {
119
+ retryAfter: Math.max(
120
+ 1,
121
+ Math.ceil(
122
+ (admissions.length >= limits.maxLookups
123
+ ? admissions[0] + limits.windowMs - now
124
+ : limits.timeoutMs) / 1000,
125
+ ),
126
+ ),
127
+ };
128
+ admissions.push(now);
129
+ const promise = run(spec);
130
+ inflight.set(spec, promise);
131
+ return { promise, reused: false };
132
+ },
133
+ close() {
134
+ shutdown.abort();
135
+ },
136
+ };
137
+ }
@@ -1,8 +1,17 @@
1
1
  import { createServer } from "node:http";
2
+ import { createRegistryFactsHandler } from "./registry-facts.mjs";
3
+ import { guardDecisionResponse } from "./guard-decision.mjs";
2
4
  import { buildPackageDetails } from "../core/package-details.mjs";
3
5
  import { attachVulnerabilityCheck } from "./enrichment.mjs";
6
+ import {
7
+ createAdvisoryService,
8
+ withAdvisoryResult,
9
+ } from "./advisory-result.mjs";
10
+ import { createDependencyScreening } from "./dependency-screening.mjs";
11
+ import { serveExport } from "./exports.mjs";
4
12
  import { createQueue, resolveLimits, systemClock } from "./queue.mjs";
5
13
  import { createReportStore } from "./store.mjs";
14
+ import { handleReportExploration } from "./report-exploration.mjs";
6
15
  import { renderReport } from "../viewer/render.mjs";
7
16
  import {
8
17
  renderHome,
@@ -102,9 +111,11 @@ export function createCacheKeyPrefix(versionInfo, limits) {
102
111
  export function createApp({
103
112
  runAnalysis,
104
113
  enrichJob,
114
+ advisoryLookup,
105
115
  versionInfo,
106
116
  limits: overrides,
107
117
  clock = systemClock,
118
+ registryFacts = {},
108
119
  } = {}) {
109
120
  if (typeof runAnalysis !== "function")
110
121
  throw new TypeError("runAnalysis is required");
@@ -119,6 +130,7 @@ export function createApp({
119
130
  );
120
131
  if (enrichJob !== undefined && typeof enrichJob !== "function")
121
132
  throw new TypeError("enrichJob must be a function");
133
+ const registryFactsHandler = createRegistryFactsHandler(registryFacts);
122
134
  const limits = resolveLimits(overrides);
123
135
  const version = Object.fromEntries(
124
136
  ["version", "commit", "tarballSha256"].map((key) => [
@@ -133,6 +145,10 @@ export function createApp({
133
145
  clock,
134
146
  });
135
147
  const keyPrefix = createCacheKeyPrefix(versionInfo, limits);
148
+ const screenDependencies = createDependencyScreening({
149
+ lookup: advisoryLookup,
150
+ clock,
151
+ });
136
152
  const queue = createQueue({
137
153
  runAnalysis,
138
154
  enrichJob,
@@ -141,6 +157,8 @@ export function createApp({
141
157
  clock,
142
158
  keyPrefix,
143
159
  });
160
+ const advisories =
161
+ enrichJob && createAdvisoryService({ lookup: enrichJob, clock });
144
162
  const server = createServer(
145
163
  { maxHeaderSize: 8192, headersTimeout: 10000, requestTimeout: 15000 },
146
164
  (req, res) => {
@@ -172,8 +190,28 @@ export function createApp({
172
190
  function error(status, title, message, headers) {
173
191
  send(status, renderError({ title, message }), undefined, headers);
174
192
  }
193
+ async function advisoryEndpoint(params) {
194
+ const spec =
195
+ params.size === 1 && parseNameAtExactVersion(params.get("package"));
196
+ if (!spec) return json(400, { error: "invalid-package-spec" });
197
+ if (!advisories)
198
+ return json(503, { error: "advisory-lookup-disabled" });
199
+ const result = advisories.request(spec);
200
+ if (!result.promise)
201
+ return json(
202
+ 429,
203
+ { error: "rate-limited", retryAfter: result.retryAfter },
204
+ { "retry-after": String(result.retryAfter) },
205
+ );
206
+ return json(200, await result.promise, {
207
+ "x-unmask-reused": String(result.reused),
208
+ });
209
+ }
175
210
  async function handle() {
176
211
  const path = req.url;
212
+ if (await registryFactsHandler(req, json)) return;
213
+ if (path === "/api/dependency-screening")
214
+ return screenDependencies(req, res, json);
177
215
  if (path === "/check") {
178
216
  if (req.method !== "POST")
179
217
  return error(
@@ -250,6 +288,25 @@ export function createApp({
250
288
  "x-unmask-reused": String(result.reused),
251
289
  });
252
290
  }
291
+ const decisionMatch =
292
+ /^\/r\/([a-f0-9]{32})\.decision\.json(\?[^#]*)?$/.exec(path);
293
+ if (decisionMatch) {
294
+ if (req.method !== "GET")
295
+ return json(405, { error: "method-unavailable" }, { allow: "GET" });
296
+ const decisionJob = queue.get(decisionMatch[1]);
297
+ // A job without a report keeps its exact-version advisory lookup inside advisoryResult.
298
+ const result = guardDecisionResponse(
299
+ decisionJob?.advisoryResult && !decisionJob.vulnerabilityCheck
300
+ ? {
301
+ ...decisionJob,
302
+ vulnerabilityCheck:
303
+ decisionJob.advisoryResult.vulnerabilityCheck,
304
+ }
305
+ : decisionJob,
306
+ { search: decisionMatch[2] ?? "" },
307
+ );
308
+ return json(result.status, result.body, result.headers);
309
+ }
253
310
  if (req.method !== "GET")
254
311
  return error(
255
312
  405,
@@ -257,9 +314,13 @@ export function createApp({
257
314
  "Use the release form to start a check.",
258
315
  { allow: "GET" },
259
316
  );
317
+ if (handleReportExploration({ path, getJob: queue.get, send })) return;
260
318
  if (path === "/") return send(200, renderHome());
261
319
  if (path === "/healthz") return json(200, { status: "ok" });
262
320
  if (path === "/version") return json(200, version);
321
+ if (path.startsWith("/api/advisory?"))
322
+ return advisoryEndpoint(new URLSearchParams(path.slice(14)));
323
+ if (serveExport({ path, queue, versionInfo: version, send })) return;
263
324
  const match = /^\/r\/([a-f0-9]{32})(\.details\.json|\.json)?$/.exec(
264
325
  path,
265
326
  );
@@ -305,7 +366,7 @@ export function createApp({
305
366
  return send(200, renderCompleted(renderReport(job.report), job));
306
367
  return send(
307
368
  200,
308
- renderStatus(job),
369
+ withAdvisoryResult(renderStatus(job), job),
309
370
  undefined,
310
371
  pending ? { "retry-after": "3" } : {},
311
372
  );
@@ -319,6 +380,9 @@ export function createApp({
319
380
  );
320
381
  },
321
382
  );
322
- server.on("close", () => queue.close());
383
+ server.on("close", () => {
384
+ queue.close();
385
+ advisories?.close();
386
+ });
323
387
  return server;
324
388
  }