@xagents_in/unmask 0.1.1 → 0.1.3
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 +220 -86
- package/dist/cli/batch.mjs +218 -0
- package/dist/cli/main.mjs +4 -0
- package/dist/cli/scan.mjs +128 -0
- package/dist/core/advisories.mjs +457 -0
- package/dist/core/package-details.mjs +638 -0
- package/dist/core/registry-facts.mjs +342 -0
- package/dist/core/report-exploration.mjs +328 -0
- package/dist/model/client.mjs +59 -32
- package/dist/server/app.mjs +37 -6
- package/dist/server/enrichment.mjs +116 -0
- package/dist/server/main.mjs +16 -0
- package/dist/server/queue.mjs +85 -6
- package/dist/server/registry-facts.mjs +118 -0
- package/dist/server/report-exploration.mjs +87 -0
- package/dist/version.json +1 -1
- package/package.json +1 -1
package/dist/model/client.mjs
CHANGED
|
@@ -9,7 +9,6 @@ const MODELS = new Set([
|
|
|
9
9
|
"gemini-3-flash-preview",
|
|
10
10
|
]);
|
|
11
11
|
export const DEFAULT_MODEL_ROUTE = Object.freeze([
|
|
12
|
-
Object.freeze({ modelId: "gemini-3.5-flash", thinkingLevel: "high" }),
|
|
13
12
|
Object.freeze({
|
|
14
13
|
modelId: "gemini-3-flash-preview",
|
|
15
14
|
thinkingLevel: "minimal",
|
|
@@ -661,28 +660,46 @@ const delay = (ms, signal) =>
|
|
|
661
660
|
function quotaScope(body, modelId) {
|
|
662
661
|
const details = body?.error?.details;
|
|
663
662
|
if (!Array.isArray(details) || details.length > 32) return "unknown";
|
|
663
|
+
const windows = [];
|
|
664
|
+
const normalize = (value) => value.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
665
|
+
function bindingWindow(binding, identity) {
|
|
666
|
+
if (/hour|week|month|year|second/.test(identity)) return null;
|
|
667
|
+
if (binding.includes("perminute") && !/day|daily/.test(identity))
|
|
668
|
+
return "minute";
|
|
669
|
+
if (binding.includes("perday") && !identity.includes("perminute"))
|
|
670
|
+
return "daily";
|
|
671
|
+
return null;
|
|
672
|
+
}
|
|
664
673
|
for (const info of details.filter(
|
|
665
674
|
(item) => item?.["@type"] === "type.googleapis.com/google.rpc.ErrorInfo",
|
|
666
675
|
)) {
|
|
667
676
|
const metadata = info.metadata;
|
|
668
|
-
const
|
|
669
|
-
`${
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
if (identity.includes("day")) return "daily";
|
|
677
|
+
const binding = normalize(
|
|
678
|
+
`${metadata?.quota_limit ?? ""} ${metadata?.quota_metric ?? ""}`,
|
|
679
|
+
);
|
|
680
|
+
const reason = normalize(String(info.reason ?? ""));
|
|
673
681
|
if (
|
|
674
682
|
["global", "project"].includes(metadata?.scope) ||
|
|
675
|
-
|
|
683
|
+
reason.includes("global") ||
|
|
684
|
+
(reason.includes("project") && !reason.includes("model")) ||
|
|
685
|
+
(binding.includes("perproject") && !binding.includes("permodel"))
|
|
676
686
|
)
|
|
677
687
|
return "global";
|
|
688
|
+
const window = bindingWindow(binding, binding + reason);
|
|
678
689
|
if (
|
|
679
690
|
!metadata ||
|
|
691
|
+
!["", "ratelimitexceeded", "quotaexceeded", "resourceexhausted"].includes(
|
|
692
|
+
reason,
|
|
693
|
+
) ||
|
|
680
694
|
![modelId, `models/${modelId}`].includes(metadata.model) ||
|
|
681
|
-
!
|
|
682
|
-
!
|
|
683
|
-
|
|
695
|
+
!binding.includes("permodel") ||
|
|
696
|
+
!window ||
|
|
697
|
+
(metadata.scope !== undefined &&
|
|
698
|
+
metadata.scope !== "model" &&
|
|
699
|
+
!(metadata.scope === "daily" && window === "daily"))
|
|
684
700
|
)
|
|
685
701
|
return "unknown";
|
|
702
|
+
windows.push(window);
|
|
686
703
|
}
|
|
687
704
|
const violations = [];
|
|
688
705
|
for (const detail of details) {
|
|
@@ -697,40 +714,34 @@ function quotaScope(body, modelId) {
|
|
|
697
714
|
violations.push(...detail.violations);
|
|
698
715
|
}
|
|
699
716
|
if (!violations.length) return "unknown";
|
|
700
|
-
if (
|
|
701
|
-
violations.some((item) =>
|
|
702
|
-
`${item?.quotaId ?? ""} ${item?.quotaMetric ?? ""}`
|
|
703
|
-
.toLowerCase()
|
|
704
|
-
.replace(/[^a-z0-9]/g, "")
|
|
705
|
-
.includes("day"),
|
|
706
|
-
)
|
|
707
|
-
)
|
|
708
|
-
return "daily";
|
|
709
717
|
for (const item of violations) {
|
|
710
718
|
const dimensions = item?.quotaDimensions;
|
|
711
719
|
if (
|
|
712
720
|
!dimensions ||
|
|
713
721
|
typeof dimensions !== "object" ||
|
|
714
722
|
Array.isArray(dimensions) ||
|
|
715
|
-
!Object.hasOwn(dimensions, "model")
|
|
723
|
+
!Object.hasOwn(dimensions, "model") ||
|
|
724
|
+
["global", "project"].includes(dimensions.scope)
|
|
716
725
|
)
|
|
717
726
|
return "global";
|
|
718
|
-
if (["global", "project", "daily"].includes(dimensions.scope))
|
|
719
|
-
return dimensions.scope === "daily" ? "daily" : "global";
|
|
720
727
|
if (![modelId, `models/${modelId}`].includes(dimensions.model))
|
|
721
728
|
return "unknown";
|
|
722
|
-
const binding =
|
|
723
|
-
.
|
|
724
|
-
|
|
729
|
+
const binding = normalize(
|
|
730
|
+
`${item.quotaId ?? ""} ${item.quotaMetric ?? ""}`,
|
|
731
|
+
);
|
|
725
732
|
if (!binding.includes("permodel"))
|
|
726
733
|
return binding.includes("perproject") ? "global" : "unknown";
|
|
734
|
+
const window = bindingWindow(binding, binding);
|
|
727
735
|
if (
|
|
728
|
-
!
|
|
729
|
-
|
|
736
|
+
!window ||
|
|
737
|
+
(dimensions.scope !== undefined &&
|
|
738
|
+
dimensions.scope !== "model" &&
|
|
739
|
+
!(dimensions.scope === "daily" && window === "daily"))
|
|
730
740
|
)
|
|
731
741
|
return "unknown";
|
|
742
|
+
windows.push(window);
|
|
732
743
|
}
|
|
733
|
-
return "model";
|
|
744
|
+
return windows.includes("daily") ? "model-daily" : "model";
|
|
734
745
|
}
|
|
735
746
|
|
|
736
747
|
export function createGemmaClient(options = {}) {
|
|
@@ -753,6 +764,11 @@ export function createGemmaClient(options = {}) {
|
|
|
753
764
|
)
|
|
754
765
|
)
|
|
755
766
|
throw new TypeError("Invalid model route");
|
|
767
|
+
if (
|
|
768
|
+
new Set(requestedRoute.map((item) => item.modelId)).size !==
|
|
769
|
+
requestedRoute.length
|
|
770
|
+
)
|
|
771
|
+
throw new TypeError("Invalid model route: duplicate model");
|
|
756
772
|
if (
|
|
757
773
|
options.route !== undefined &&
|
|
758
774
|
options.modelId !== undefined &&
|
|
@@ -795,10 +811,16 @@ export function createGemmaClient(options = {}) {
|
|
|
795
811
|
}));
|
|
796
812
|
const generationConfig = route[0].generationConfig;
|
|
797
813
|
const routingPolicy = {
|
|
798
|
-
version: "ordered-fallback-
|
|
814
|
+
version: "ordered-fallback-2",
|
|
799
815
|
maxGenerationAttempts: Math.max(2, route.length),
|
|
800
|
-
fallthrough: [
|
|
816
|
+
fallthrough: [
|
|
817
|
+
"model-scoped-minute-429",
|
|
818
|
+
"model-scoped-daily-429",
|
|
819
|
+
"transient-5xx",
|
|
820
|
+
"timeout",
|
|
821
|
+
],
|
|
801
822
|
invalidOutputRetry: "same-tier",
|
|
823
|
+
modelDailyFallback: "next-tier-no-wait",
|
|
802
824
|
};
|
|
803
825
|
const fetchImpl = options.fetchImpl ?? globalThis.fetch;
|
|
804
826
|
const now = options.now ?? (() => performance.now());
|
|
@@ -1108,12 +1130,17 @@ export function createGemmaClient(options = {}) {
|
|
|
1108
1130
|
body = await jsonResponse(response, controller.signal);
|
|
1109
1131
|
} catch {}
|
|
1110
1132
|
failureScope = quotaScope(body, tier.modelId);
|
|
1111
|
-
const retry =
|
|
1133
|
+
const retry =
|
|
1134
|
+
failureScope === "model" ||
|
|
1135
|
+
(failureScope === "model-daily" &&
|
|
1136
|
+
tierIndex + 1 < route.length);
|
|
1112
1137
|
throw Object.assign(
|
|
1113
1138
|
new Failure(
|
|
1114
1139
|
"quota",
|
|
1115
1140
|
retry,
|
|
1116
|
-
|
|
1141
|
+
failureScope === "model-daily"
|
|
1142
|
+
? 0
|
|
1143
|
+
: retryDelay(response, wallNow()),
|
|
1117
1144
|
retry,
|
|
1118
1145
|
),
|
|
1119
1146
|
{ scope: failureScope },
|
package/dist/server/app.mjs
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { createServer } from "node:http";
|
|
2
|
+
import { createRegistryFactsHandler } from "./registry-facts.mjs";
|
|
3
|
+
import { buildPackageDetails } from "../core/package-details.mjs";
|
|
4
|
+
import { attachVulnerabilityCheck } from "./enrichment.mjs";
|
|
2
5
|
import { createQueue, resolveLimits, systemClock } from "./queue.mjs";
|
|
3
6
|
import { createReportStore } from "./store.mjs";
|
|
7
|
+
import { handleReportExploration } from "./report-exploration.mjs";
|
|
4
8
|
import { renderReport } from "../viewer/render.mjs";
|
|
5
9
|
import {
|
|
6
10
|
renderHome,
|
|
@@ -99,9 +103,11 @@ export function createCacheKeyPrefix(versionInfo, limits) {
|
|
|
99
103
|
|
|
100
104
|
export function createApp({
|
|
101
105
|
runAnalysis,
|
|
106
|
+
enrichJob,
|
|
102
107
|
versionInfo,
|
|
103
108
|
limits: overrides,
|
|
104
109
|
clock = systemClock,
|
|
110
|
+
registryFacts = {},
|
|
105
111
|
} = {}) {
|
|
106
112
|
if (typeof runAnalysis !== "function")
|
|
107
113
|
throw new TypeError("runAnalysis is required");
|
|
@@ -114,6 +120,9 @@ export function createApp({
|
|
|
114
120
|
throw new TypeError(
|
|
115
121
|
"versionInfo requires version, commit and tarballSha256 strings",
|
|
116
122
|
);
|
|
123
|
+
if (enrichJob !== undefined && typeof enrichJob !== "function")
|
|
124
|
+
throw new TypeError("enrichJob must be a function");
|
|
125
|
+
const registryFactsHandler = createRegistryFactsHandler(registryFacts);
|
|
117
126
|
const limits = resolveLimits(overrides);
|
|
118
127
|
const version = Object.fromEntries(
|
|
119
128
|
["version", "commit", "tarballSha256"].map((key) => [
|
|
@@ -128,7 +137,14 @@ export function createApp({
|
|
|
128
137
|
clock,
|
|
129
138
|
});
|
|
130
139
|
const keyPrefix = createCacheKeyPrefix(versionInfo, limits);
|
|
131
|
-
const queue = createQueue({
|
|
140
|
+
const queue = createQueue({
|
|
141
|
+
runAnalysis,
|
|
142
|
+
enrichJob,
|
|
143
|
+
store,
|
|
144
|
+
limits,
|
|
145
|
+
clock,
|
|
146
|
+
keyPrefix,
|
|
147
|
+
});
|
|
132
148
|
const server = createServer(
|
|
133
149
|
{ maxHeaderSize: 8192, headersTimeout: 10000, requestTimeout: 15000 },
|
|
134
150
|
(req, res) => {
|
|
@@ -162,6 +178,7 @@ export function createApp({
|
|
|
162
178
|
}
|
|
163
179
|
async function handle() {
|
|
164
180
|
const path = req.url;
|
|
181
|
+
if (await registryFactsHandler(req, json)) return;
|
|
165
182
|
if (path === "/check") {
|
|
166
183
|
if (req.method !== "POST")
|
|
167
184
|
return error(
|
|
@@ -245,10 +262,13 @@ export function createApp({
|
|
|
245
262
|
"Use the release form to start a check.",
|
|
246
263
|
{ allow: "GET" },
|
|
247
264
|
);
|
|
265
|
+
if (handleReportExploration({ path, getJob: queue.get, send })) return;
|
|
248
266
|
if (path === "/") return send(200, renderHome());
|
|
249
267
|
if (path === "/healthz") return json(200, { status: "ok" });
|
|
250
268
|
if (path === "/version") return json(200, version);
|
|
251
|
-
const match = /^\/r\/([a-f0-9]{32})(\.json)?$/.exec(
|
|
269
|
+
const match = /^\/r\/([a-f0-9]{32})(\.details\.json|\.json)?$/.exec(
|
|
270
|
+
path,
|
|
271
|
+
);
|
|
252
272
|
if (!match)
|
|
253
273
|
return error(
|
|
254
274
|
404,
|
|
@@ -265,10 +285,21 @@ export function createApp({
|
|
|
265
285
|
const pending = ["queued", "running"].includes(job.status);
|
|
266
286
|
if (match[2]) {
|
|
267
287
|
if (job.report)
|
|
268
|
-
return json(
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
288
|
+
return json(
|
|
289
|
+
200,
|
|
290
|
+
match[2] === ".details.json"
|
|
291
|
+
? attachVulnerabilityCheck(
|
|
292
|
+
buildPackageDetails(job.report),
|
|
293
|
+
job.vulnerabilityCheck,
|
|
294
|
+
)
|
|
295
|
+
: job.report,
|
|
296
|
+
{
|
|
297
|
+
"content-disposition":
|
|
298
|
+
match[2] === ".details.json"
|
|
299
|
+
? 'attachment; filename="unmask-package-details.json"'
|
|
300
|
+
: 'attachment; filename="unmask-report.json"',
|
|
301
|
+
},
|
|
302
|
+
);
|
|
272
303
|
const { key, ...publicJob } = job;
|
|
273
304
|
return json(
|
|
274
305
|
pending ? 202 : 200,
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
export const DETAILS_RESPONSE_LIMITS = Object.freeze({
|
|
2
|
+
packageDetailsBytes: 2097152,
|
|
3
|
+
vulnerabilityCheckBytes: 2096128,
|
|
4
|
+
combinedBytes: 4194304,
|
|
5
|
+
});
|
|
6
|
+
|
|
7
|
+
export function unavailableVulnerabilityCheck(spec, clock, reason) {
|
|
8
|
+
const separator = typeof spec === "string" ? spec.lastIndexOf("@") : -1;
|
|
9
|
+
return {
|
|
10
|
+
schemaVersion: "1",
|
|
11
|
+
source: "OSV",
|
|
12
|
+
package:
|
|
13
|
+
separator > 0
|
|
14
|
+
? {
|
|
15
|
+
ecosystem: "npm",
|
|
16
|
+
name: spec.slice(0, separator),
|
|
17
|
+
version: spec.slice(separator + 1),
|
|
18
|
+
}
|
|
19
|
+
: null,
|
|
20
|
+
queriedAt: new Date(clock.now()).toISOString(),
|
|
21
|
+
status: "unavailable",
|
|
22
|
+
complete: false,
|
|
23
|
+
reasons: [reason],
|
|
24
|
+
advisories: [],
|
|
25
|
+
pagesFetched: 0,
|
|
26
|
+
limits: { resultBytes: DETAILS_RESPONSE_LIMITS.vulnerabilityCheckBytes },
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function boundedVulnerabilityCheck(value, spec, clock) {
|
|
31
|
+
try {
|
|
32
|
+
const json = JSON.stringify(value);
|
|
33
|
+
if (
|
|
34
|
+
Buffer.byteLength(json) > DETAILS_RESPONSE_LIMITS.vulnerabilityCheckBytes
|
|
35
|
+
)
|
|
36
|
+
return unavailableVulnerabilityCheck(spec, clock, "result-byte-cap");
|
|
37
|
+
const snapshot = JSON.parse(json);
|
|
38
|
+
if (
|
|
39
|
+
snapshot.schemaVersion !== "1" ||
|
|
40
|
+
snapshot.source !== "OSV" ||
|
|
41
|
+
!["found", "none-reported", "incomplete", "unavailable"].includes(
|
|
42
|
+
snapshot.status,
|
|
43
|
+
) ||
|
|
44
|
+
snapshot.complete !==
|
|
45
|
+
["found", "none-reported"].includes(snapshot.status) ||
|
|
46
|
+
!Array.isArray(snapshot.reasons) ||
|
|
47
|
+
!snapshot.reasons.every((reason) => typeof reason === "string") ||
|
|
48
|
+
!Array.isArray(snapshot.advisories) ||
|
|
49
|
+
snapshot.advisories.some(
|
|
50
|
+
(item) => !item || typeof item !== "object" || Array.isArray(item),
|
|
51
|
+
) ||
|
|
52
|
+
(snapshot.status === "found" && snapshot.advisories.length === 0) ||
|
|
53
|
+
(snapshot.status === "none-reported" &&
|
|
54
|
+
snapshot.advisories.length !== 0) ||
|
|
55
|
+
!Number.isSafeInteger(snapshot.pagesFetched) ||
|
|
56
|
+
snapshot.pagesFetched < 0 ||
|
|
57
|
+
(snapshot.complete &&
|
|
58
|
+
(snapshot.pagesFetched === 0 || snapshot.reasons.length !== 0)) ||
|
|
59
|
+
typeof snapshot.queriedAt !== "string" ||
|
|
60
|
+
!Number.isFinite(Date.parse(snapshot.queriedAt)) ||
|
|
61
|
+
!snapshot.limits ||
|
|
62
|
+
typeof snapshot.limits !== "object"
|
|
63
|
+
)
|
|
64
|
+
throw new TypeError("Invalid vulnerability enrichment");
|
|
65
|
+
const expected = unavailableVulnerabilityCheck(
|
|
66
|
+
spec,
|
|
67
|
+
clock,
|
|
68
|
+
"unused",
|
|
69
|
+
).package;
|
|
70
|
+
if (
|
|
71
|
+
snapshot.package !== null &&
|
|
72
|
+
(snapshot.package.ecosystem !== "npm" ||
|
|
73
|
+
snapshot.package.name !== expected?.name ||
|
|
74
|
+
snapshot.package.version !== expected?.version)
|
|
75
|
+
)
|
|
76
|
+
throw new TypeError("Vulnerability enrichment identity mismatch");
|
|
77
|
+
if (snapshot.complete && snapshot.package === null)
|
|
78
|
+
throw new TypeError("Vulnerability enrichment identity missing");
|
|
79
|
+
return snapshot;
|
|
80
|
+
} catch {
|
|
81
|
+
return unavailableVulnerabilityCheck(spec, clock, "invalid-result");
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function attachVulnerabilityCheck(details, value) {
|
|
86
|
+
const vulnerabilityCheck = value ?? {
|
|
87
|
+
source: "OSV",
|
|
88
|
+
status: "not-assessed",
|
|
89
|
+
complete: false,
|
|
90
|
+
reasons: ["hosted-enrichment-disabled"],
|
|
91
|
+
advisories: [],
|
|
92
|
+
};
|
|
93
|
+
const complete =
|
|
94
|
+
vulnerabilityCheck.complete === true &&
|
|
95
|
+
["found", "none-reported"].includes(vulnerabilityCheck.status);
|
|
96
|
+
const result = {
|
|
97
|
+
...details,
|
|
98
|
+
scope: {
|
|
99
|
+
...details.scope,
|
|
100
|
+
vulnerabilityScan: complete
|
|
101
|
+
? "OSV-exact-package-query-complete"
|
|
102
|
+
: "not-assessed",
|
|
103
|
+
knownVulnerabilities: complete
|
|
104
|
+
? "queried-exact-package-only"
|
|
105
|
+
: "not-assessed",
|
|
106
|
+
},
|
|
107
|
+
vulnerabilityCheck,
|
|
108
|
+
responseLimits: DETAILS_RESPONSE_LIMITS,
|
|
109
|
+
};
|
|
110
|
+
if (
|
|
111
|
+
Buffer.byteLength(JSON.stringify(result)) >
|
|
112
|
+
DETAILS_RESPONSE_LIMITS.combinedBytes
|
|
113
|
+
)
|
|
114
|
+
throw new TypeError("Combined details response exceeds cap");
|
|
115
|
+
return result;
|
|
116
|
+
}
|
package/dist/server/main.mjs
CHANGED
|
@@ -47,6 +47,7 @@ export async function startServer({
|
|
|
47
47
|
runnerOptions = {},
|
|
48
48
|
versionInfo,
|
|
49
49
|
modelClient,
|
|
50
|
+
enrichJob,
|
|
50
51
|
} = {}) {
|
|
51
52
|
if (
|
|
52
53
|
typeof host !== "string" ||
|
|
@@ -62,6 +63,14 @@ export async function startServer({
|
|
|
62
63
|
: modelClient === undefined
|
|
63
64
|
? await modelClientFromEnv()
|
|
64
65
|
: modelClient;
|
|
66
|
+
const enrichment =
|
|
67
|
+
enrichJob === null
|
|
68
|
+
? undefined
|
|
69
|
+
: enrichJob === undefined
|
|
70
|
+
? (await import("../core/advisories.mjs")).lookupNpmAdvisories
|
|
71
|
+
: enrichJob;
|
|
72
|
+
if (enrichment !== undefined && typeof enrichment !== "function")
|
|
73
|
+
throw new TypeError("enrichJob must be a function or null");
|
|
65
74
|
const run = await createRunner({
|
|
66
75
|
...runnerOptions,
|
|
67
76
|
tempRoot,
|
|
@@ -81,6 +90,13 @@ export async function startServer({
|
|
|
81
90
|
};
|
|
82
91
|
const server = createApp({
|
|
83
92
|
runAnalysis,
|
|
93
|
+
enrichJob:
|
|
94
|
+
enrichment === undefined
|
|
95
|
+
? undefined
|
|
96
|
+
: (spec, { signal }) =>
|
|
97
|
+
enrichment(spec, {
|
|
98
|
+
signal: AbortSignal.any([signal, shutdown.signal]),
|
|
99
|
+
}),
|
|
84
100
|
versionInfo: {
|
|
85
101
|
...(versionInfo || (await loadVersionInfo())),
|
|
86
102
|
modelConfigDigest:
|
package/dist/server/queue.mjs
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import { randomBytes } from "node:crypto";
|
|
2
|
+
import {
|
|
3
|
+
boundedVulnerabilityCheck,
|
|
4
|
+
unavailableVulnerabilityCheck,
|
|
5
|
+
} from "./enrichment.mjs";
|
|
2
6
|
|
|
3
7
|
export const DEFAULT_LIMITS = Object.freeze({
|
|
4
8
|
maxBodyBytes: 4096,
|
|
@@ -28,6 +32,7 @@ export function resolveLimits(overrides = {}) {
|
|
|
28
32
|
|
|
29
33
|
export function createQueue({
|
|
30
34
|
runAnalysis,
|
|
35
|
+
enrichJob,
|
|
31
36
|
store,
|
|
32
37
|
limits,
|
|
33
38
|
clock = systemClock,
|
|
@@ -39,7 +44,20 @@ export function createQueue({
|
|
|
39
44
|
let admissions = [];
|
|
40
45
|
function persist(job) {
|
|
41
46
|
if (!store.put(job)) {
|
|
42
|
-
|
|
47
|
+
if (
|
|
48
|
+
job.report &&
|
|
49
|
+
job.vulnerabilityCheck &&
|
|
50
|
+
store.put({
|
|
51
|
+
...job,
|
|
52
|
+
vulnerabilityCheck: unavailableVulnerabilityCheck(
|
|
53
|
+
job.nameAtExactVersion,
|
|
54
|
+
clock,
|
|
55
|
+
"storage-byte-cap",
|
|
56
|
+
),
|
|
57
|
+
})
|
|
58
|
+
)
|
|
59
|
+
return;
|
|
60
|
+
const { report, vulnerabilityCheck, ...small } = job;
|
|
43
61
|
store.put({ ...small, status: "incomplete", reason: "report-too-large" });
|
|
44
62
|
}
|
|
45
63
|
}
|
|
@@ -61,12 +79,26 @@ export function createQueue({
|
|
|
61
79
|
job.status = "running";
|
|
62
80
|
job.startedAt = clock.now();
|
|
63
81
|
const controller = new AbortController();
|
|
82
|
+
let coreReport;
|
|
64
83
|
const timer = clock.setTimeout(() => {
|
|
65
|
-
job.status = "incomplete";
|
|
66
|
-
job.reason = "execution-timeout";
|
|
67
84
|
job.finishedAt = clock.now();
|
|
85
|
+
if (coreReport) {
|
|
86
|
+
job.status = coreReport.policy.exitCode === 2 ? "incomplete" : "done";
|
|
87
|
+
persist({
|
|
88
|
+
...publicJob(job),
|
|
89
|
+
report: coreReport,
|
|
90
|
+
vulnerabilityCheck: unavailableVulnerabilityCheck(
|
|
91
|
+
job.nameAtExactVersion,
|
|
92
|
+
clock,
|
|
93
|
+
"aborted",
|
|
94
|
+
),
|
|
95
|
+
});
|
|
96
|
+
} else {
|
|
97
|
+
job.status = "incomplete";
|
|
98
|
+
job.reason = "execution-timeout";
|
|
99
|
+
persist(job);
|
|
100
|
+
}
|
|
68
101
|
controller.abort();
|
|
69
|
-
persist(job);
|
|
70
102
|
}, limits.executionMs);
|
|
71
103
|
timer?.unref?.();
|
|
72
104
|
job.cancel = () => {
|
|
@@ -77,7 +109,7 @@ export function createQueue({
|
|
|
77
109
|
.then(() =>
|
|
78
110
|
runAnalysis(job.nameAtExactVersion, { signal: controller.signal }),
|
|
79
111
|
)
|
|
80
|
-
.then((report) => {
|
|
112
|
+
.then(async (report) => {
|
|
81
113
|
if (job.status !== "running" || closed) return;
|
|
82
114
|
if (
|
|
83
115
|
!report ||
|
|
@@ -86,9 +118,56 @@ export function createQueue({
|
|
|
86
118
|
![0, 2, 3].includes(report.policy?.exitCode)
|
|
87
119
|
)
|
|
88
120
|
throw new TypeError("Invalid report");
|
|
121
|
+
coreReport = report;
|
|
122
|
+
let vulnerabilityCheck;
|
|
123
|
+
if (enrichJob) {
|
|
124
|
+
let abort;
|
|
125
|
+
const interrupted = new Promise((resolve) => {
|
|
126
|
+
abort = () =>
|
|
127
|
+
resolve(
|
|
128
|
+
unavailableVulnerabilityCheck(
|
|
129
|
+
job.nameAtExactVersion,
|
|
130
|
+
clock,
|
|
131
|
+
"aborted",
|
|
132
|
+
),
|
|
133
|
+
);
|
|
134
|
+
controller.signal.addEventListener("abort", abort, { once: true });
|
|
135
|
+
if (controller.signal.aborted) abort();
|
|
136
|
+
});
|
|
137
|
+
try {
|
|
138
|
+
const value = await Promise.race([
|
|
139
|
+
Promise.resolve().then(() =>
|
|
140
|
+
enrichJob(job.nameAtExactVersion, {
|
|
141
|
+
signal: controller.signal,
|
|
142
|
+
}),
|
|
143
|
+
),
|
|
144
|
+
interrupted,
|
|
145
|
+
]);
|
|
146
|
+
vulnerabilityCheck = boundedVulnerabilityCheck(
|
|
147
|
+
value,
|
|
148
|
+
job.nameAtExactVersion,
|
|
149
|
+
clock,
|
|
150
|
+
);
|
|
151
|
+
} catch {
|
|
152
|
+
vulnerabilityCheck = unavailableVulnerabilityCheck(
|
|
153
|
+
job.nameAtExactVersion,
|
|
154
|
+
clock,
|
|
155
|
+
"lookup-failed",
|
|
156
|
+
);
|
|
157
|
+
} finally {
|
|
158
|
+
controller.signal.removeEventListener("abort", abort);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (job.status !== "running" || closed) return;
|
|
89
162
|
const status = report.policy.exitCode === 2 ? "incomplete" : "done";
|
|
90
163
|
const finishedAt = clock.now();
|
|
91
|
-
persist({
|
|
164
|
+
persist({
|
|
165
|
+
...publicJob(job),
|
|
166
|
+
status,
|
|
167
|
+
report,
|
|
168
|
+
finishedAt,
|
|
169
|
+
...(vulnerabilityCheck ? { vulnerabilityCheck } : {}),
|
|
170
|
+
});
|
|
92
171
|
job.status = status;
|
|
93
172
|
job.finishedAt = finishedAt;
|
|
94
173
|
})
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getRegistryFacts,
|
|
3
|
+
getRegistryHistory,
|
|
4
|
+
registryIdentity,
|
|
5
|
+
} from "../core/registry-facts.mjs";
|
|
6
|
+
|
|
7
|
+
export function createRegistryFactsHandler(options = {}) {
|
|
8
|
+
const maxConcurrent = options.maxConcurrent ?? 2;
|
|
9
|
+
const maxRequests = options.maxRequests ?? 30;
|
|
10
|
+
if (
|
|
11
|
+
!Number.isSafeInteger(maxConcurrent) ||
|
|
12
|
+
maxConcurrent < 1 ||
|
|
13
|
+
maxConcurrent > 2 ||
|
|
14
|
+
!Number.isSafeInteger(maxRequests) ||
|
|
15
|
+
maxRequests < 1 ||
|
|
16
|
+
maxRequests > 30
|
|
17
|
+
)
|
|
18
|
+
throw new TypeError("Invalid registry lookup admission limits");
|
|
19
|
+
let active = 0,
|
|
20
|
+
used = 0,
|
|
21
|
+
windowStart = Date.now();
|
|
22
|
+
return async function handleRegistryFacts(req, json) {
|
|
23
|
+
const url = new URL(req.url, "http://localhost");
|
|
24
|
+
const history = url.pathname === "/api/registry/history";
|
|
25
|
+
if (url.pathname !== "/api/registry/facts" && !history) return false;
|
|
26
|
+
if (req.method !== "GET") {
|
|
27
|
+
json(
|
|
28
|
+
405,
|
|
29
|
+
{ status: "rejected", reason: "method-not-allowed" },
|
|
30
|
+
{ allow: "GET" },
|
|
31
|
+
);
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
const spec = url.searchParams.get("package");
|
|
35
|
+
let limit = 20;
|
|
36
|
+
try {
|
|
37
|
+
const allowed = history ? ["package", "limit"] : ["package"];
|
|
38
|
+
if (
|
|
39
|
+
!spec ||
|
|
40
|
+
[...url.searchParams.keys()].some((key) => !allowed.includes(key)) ||
|
|
41
|
+
allowed.some((key) => url.searchParams.getAll(key).length > 1)
|
|
42
|
+
)
|
|
43
|
+
throw new Error();
|
|
44
|
+
if (history) {
|
|
45
|
+
registryIdentity(`${spec}@0.0.0`);
|
|
46
|
+
if (url.searchParams.has("limit")) {
|
|
47
|
+
const raw = url.searchParams.get("limit");
|
|
48
|
+
if (!/^[1-9]\d{0,2}$/.test(raw)) throw new Error();
|
|
49
|
+
limit = Number(raw);
|
|
50
|
+
if (limit > 100) throw new Error();
|
|
51
|
+
}
|
|
52
|
+
} else registryIdentity(spec);
|
|
53
|
+
} catch {
|
|
54
|
+
json(400, {
|
|
55
|
+
status: "rejected",
|
|
56
|
+
reason: history
|
|
57
|
+
? "package-name-and-bounded-limit-required"
|
|
58
|
+
: "exact-package-required",
|
|
59
|
+
});
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
const now = Date.now();
|
|
63
|
+
if (now - windowStart >= 60000) {
|
|
64
|
+
used = 0;
|
|
65
|
+
windowStart = now;
|
|
66
|
+
}
|
|
67
|
+
if (active >= maxConcurrent || used >= maxRequests) {
|
|
68
|
+
json(
|
|
69
|
+
429,
|
|
70
|
+
{ status: "unavailable", reason: "registry-lookup-limit" },
|
|
71
|
+
{
|
|
72
|
+
"retry-after": String(
|
|
73
|
+
Math.max(1, Math.ceil((60000 - (now - windowStart)) / 1000)),
|
|
74
|
+
),
|
|
75
|
+
},
|
|
76
|
+
);
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
used++;
|
|
80
|
+
active++;
|
|
81
|
+
try {
|
|
82
|
+
json(
|
|
83
|
+
200,
|
|
84
|
+
history
|
|
85
|
+
? await getRegistryHistory(spec, { ...options, limit })
|
|
86
|
+
: await getRegistryFacts(spec, options),
|
|
87
|
+
);
|
|
88
|
+
} catch (error) {
|
|
89
|
+
const reason = [
|
|
90
|
+
"acquisition-timeout",
|
|
91
|
+
"response-cap",
|
|
92
|
+
"redirect-refused",
|
|
93
|
+
"invalid-registry-document",
|
|
94
|
+
"registry-identity-mismatch",
|
|
95
|
+
"http-status",
|
|
96
|
+
"network-failure",
|
|
97
|
+
"invalid-response",
|
|
98
|
+
"registry-history-cap",
|
|
99
|
+
].includes(error.code)
|
|
100
|
+
? error.code
|
|
101
|
+
: "registry-unavailable";
|
|
102
|
+
const status =
|
|
103
|
+
reason === "acquisition-timeout"
|
|
104
|
+
? 504
|
|
105
|
+
: error.observation?.status === 404
|
|
106
|
+
? 404
|
|
107
|
+
: 502;
|
|
108
|
+
json(status, {
|
|
109
|
+
status: "unavailable",
|
|
110
|
+
reason,
|
|
111
|
+
safetyVerdict: "not-provided",
|
|
112
|
+
});
|
|
113
|
+
} finally {
|
|
114
|
+
active--;
|
|
115
|
+
}
|
|
116
|
+
return true;
|
|
117
|
+
};
|
|
118
|
+
}
|