@xagents_in/unmask 0.1.2 → 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/registry-facts.mjs +342 -0
- package/dist/core/report-exploration.mjs +328 -0
- package/dist/server/app.mjs +6 -0
- 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
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
const states = new Map([
|
|
4
|
+
["changed", "changed"],
|
|
5
|
+
["artifact-only", "added"],
|
|
6
|
+
["byte-identical", "identical"],
|
|
7
|
+
["not-compared", "not-compared"],
|
|
8
|
+
]);
|
|
9
|
+
const digest = (value) => (/^[a-f0-9]{64}$/.test(value ?? "") ? value : null);
|
|
10
|
+
const order = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
|
|
11
|
+
export const EXPLORATION_BYTE_LIMIT = 131072;
|
|
12
|
+
export const explorationLimitations = [
|
|
13
|
+
"States compare artifact and selected source, not releases or version history.",
|
|
14
|
+
"Source-only paths are recorded observations; their hashes, file types and excerpts may be unavailable.",
|
|
15
|
+
"Incomplete inventories and unassessed files do not establish safety or absence.",
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
export function safeExplorationPath(value, { prefix = false } = {}) {
|
|
19
|
+
if (
|
|
20
|
+
typeof value !== "string" ||
|
|
21
|
+
value.length > 4096 ||
|
|
22
|
+
/[\x00-\x1f\x7f\\%:]/.test(value)
|
|
23
|
+
)
|
|
24
|
+
return false;
|
|
25
|
+
if (prefix && value === "") return true;
|
|
26
|
+
const parts = (
|
|
27
|
+
prefix && value.endsWith("/") ? value.slice(0, -1) : value
|
|
28
|
+
).split("/");
|
|
29
|
+
return parts.every((part) => part && part !== "." && part !== "..");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function explorationJson(value) {
|
|
33
|
+
return JSON.stringify(value).replace(
|
|
34
|
+
/[<>&\u2028\u2029]/g,
|
|
35
|
+
(character) =>
|
|
36
|
+
`\\u${character.charCodeAt(0).toString(16).padStart(4, "0")}`,
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function reportFileRows(report, { reportId } = {}) {
|
|
41
|
+
if (reportId !== undefined && !/^[a-f0-9]{32}$/.test(reportId))
|
|
42
|
+
throw new TypeError("Invalid report identity");
|
|
43
|
+
const rows = [];
|
|
44
|
+
const paths = new Set();
|
|
45
|
+
let omitted = 0;
|
|
46
|
+
for (const file of report.files ?? []) {
|
|
47
|
+
if (
|
|
48
|
+
!states.has(file.state) ||
|
|
49
|
+
!safeExplorationPath(file.originalPath) ||
|
|
50
|
+
(file.path !== null && !safeExplorationPath(file.path))
|
|
51
|
+
) {
|
|
52
|
+
omitted++;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (file.path !== null) paths.add(file.path);
|
|
56
|
+
rows.push({
|
|
57
|
+
path: file.path,
|
|
58
|
+
originalPath: file.originalPath,
|
|
59
|
+
state: states.get(file.state),
|
|
60
|
+
comparisonState: file.state,
|
|
61
|
+
size:
|
|
62
|
+
Number.isSafeInteger(file.size) && file.size >= 0 ? file.size : null,
|
|
63
|
+
artifactSha256: digest(file.artifactSha256),
|
|
64
|
+
sourceSha256: digest(file.sourceSha256),
|
|
65
|
+
reason:
|
|
66
|
+
typeof file.reason === "string" ? file.reason.slice(0, 128) : null,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
for (const path of new Set(report.coverage?.sourceOnly ?? [])) {
|
|
70
|
+
if (!safeExplorationPath(path)) {
|
|
71
|
+
omitted++;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (paths.has(path)) continue;
|
|
75
|
+
rows.push({
|
|
76
|
+
path,
|
|
77
|
+
originalPath: null,
|
|
78
|
+
state: "removed",
|
|
79
|
+
comparisonState: "source-only",
|
|
80
|
+
size: null,
|
|
81
|
+
artifactSha256: null,
|
|
82
|
+
sourceSha256: null,
|
|
83
|
+
reason: "recorded-source-only",
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
rows.sort(
|
|
87
|
+
(a, b) =>
|
|
88
|
+
order(a.path ?? a.originalPath, b.path ?? b.originalPath) ||
|
|
89
|
+
order(a.originalPath ?? "", b.originalPath ?? ""),
|
|
90
|
+
);
|
|
91
|
+
if (reportId !== undefined)
|
|
92
|
+
for (const file of rows) {
|
|
93
|
+
file.fileId = `f-${createHash("sha256")
|
|
94
|
+
.update(
|
|
95
|
+
JSON.stringify([
|
|
96
|
+
"unmask-file-id-v1",
|
|
97
|
+
reportId,
|
|
98
|
+
file.path,
|
|
99
|
+
file.originalPath,
|
|
100
|
+
]),
|
|
101
|
+
)
|
|
102
|
+
.digest("base64url")}`;
|
|
103
|
+
}
|
|
104
|
+
return { rows, omitted };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function buildReportFileInventory(
|
|
108
|
+
report,
|
|
109
|
+
{ state = "all", prefix = "", offset = 0, limit = 50 } = {},
|
|
110
|
+
context = {},
|
|
111
|
+
) {
|
|
112
|
+
if (
|
|
113
|
+
!["all", "removed", ...states.values()].includes(state) ||
|
|
114
|
+
!safeExplorationPath(prefix, { prefix: true }) ||
|
|
115
|
+
!Number.isSafeInteger(offset) ||
|
|
116
|
+
offset < 0 ||
|
|
117
|
+
!Number.isSafeInteger(limit) ||
|
|
118
|
+
limit < 1 ||
|
|
119
|
+
limit > 100
|
|
120
|
+
)
|
|
121
|
+
throw new TypeError("Invalid inventory query");
|
|
122
|
+
const { rows, omitted } = reportFileRows(report, context);
|
|
123
|
+
const filtered = rows.filter(
|
|
124
|
+
(file) =>
|
|
125
|
+
(state === "all" || file.state === state) &&
|
|
126
|
+
(file.path ?? file.originalPath).startsWith(prefix),
|
|
127
|
+
);
|
|
128
|
+
const files = [];
|
|
129
|
+
let bytes = 0;
|
|
130
|
+
for (const file of filtered.slice(offset, offset + limit)) {
|
|
131
|
+
const size = Buffer.byteLength(explorationJson(file)) + 1;
|
|
132
|
+
if (
|
|
133
|
+
bytes + size >
|
|
134
|
+
EXPLORATION_BYTE_LIMIT -
|
|
135
|
+
Buffer.byteLength(explorationJson({ prefix })) -
|
|
136
|
+
8192
|
|
137
|
+
)
|
|
138
|
+
break;
|
|
139
|
+
bytes += size;
|
|
140
|
+
files.push(file);
|
|
141
|
+
}
|
|
142
|
+
return {
|
|
143
|
+
schemaVersion: "1",
|
|
144
|
+
scope: "artifact-source",
|
|
145
|
+
state,
|
|
146
|
+
prefix,
|
|
147
|
+
offset,
|
|
148
|
+
limit,
|
|
149
|
+
total: filtered.length,
|
|
150
|
+
files,
|
|
151
|
+
nextOffset:
|
|
152
|
+
offset + files.length < filtered.length ? offset + files.length : null,
|
|
153
|
+
omittedUnsafePaths: omitted,
|
|
154
|
+
inventoryComplete: report.coverage?.inventory?.complete === true,
|
|
155
|
+
sourceComplete: report.coverage?.sourceComplete === true,
|
|
156
|
+
limitations: explorationLimitations,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function validRange(value, size) {
|
|
161
|
+
return (
|
|
162
|
+
Array.isArray(value) &&
|
|
163
|
+
value.length === 2 &&
|
|
164
|
+
value.every((number) => Number.isSafeInteger(number) && number >= 0) &&
|
|
165
|
+
value[0] <= value[1] &&
|
|
166
|
+
value[1] <= size
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function recordedExcerpt(item) {
|
|
171
|
+
const unavailable = {
|
|
172
|
+
excerpt: null,
|
|
173
|
+
excerptRange: null,
|
|
174
|
+
recordedExcerptRange: null,
|
|
175
|
+
excerptTruncated: false,
|
|
176
|
+
excerptStatus: "not-recorded",
|
|
177
|
+
};
|
|
178
|
+
if (item.excerpt === undefined) return unavailable;
|
|
179
|
+
if (
|
|
180
|
+
typeof item.excerpt !== "string" ||
|
|
181
|
+
!validRange(item.excerptRange, item.size) ||
|
|
182
|
+
item.excerptRange[0] !== item.range[0] ||
|
|
183
|
+
item.excerptRange[1] > item.range[1] ||
|
|
184
|
+
Buffer.byteLength(item.excerpt) !==
|
|
185
|
+
item.excerptRange[1] - item.excerptRange[0]
|
|
186
|
+
)
|
|
187
|
+
return { ...unavailable, excerptStatus: "invalid-recorded-range" };
|
|
188
|
+
let excerpt = "",
|
|
189
|
+
bytes = 0;
|
|
190
|
+
for (const character of item.excerpt) {
|
|
191
|
+
const size = Buffer.byteLength(character);
|
|
192
|
+
if (bytes + size > 2048) break;
|
|
193
|
+
bytes += size;
|
|
194
|
+
excerpt += character;
|
|
195
|
+
}
|
|
196
|
+
return {
|
|
197
|
+
excerpt,
|
|
198
|
+
excerptRange: [item.excerptRange[0], item.excerptRange[0] + bytes],
|
|
199
|
+
recordedExcerptRange: item.excerptRange,
|
|
200
|
+
excerptTruncated: bytes < Buffer.byteLength(item.excerpt),
|
|
201
|
+
excerptStatus: "recorded",
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function buildReportFileEvidence(report, selection, context = {}) {
|
|
206
|
+
const keys = Object.keys(selection ?? {});
|
|
207
|
+
if (
|
|
208
|
+
keys.length !== 1 ||
|
|
209
|
+
!["path", "originalPath", "fileId"].includes(keys[0]) ||
|
|
210
|
+
(keys[0] === "fileId"
|
|
211
|
+
? !/^[a-f0-9]{32}$/.test(context.reportId ?? "") ||
|
|
212
|
+
!/^f-[A-Za-z0-9_-]{43}$/.test(selection.fileId)
|
|
213
|
+
: !safeExplorationPath(selection[keys[0]]))
|
|
214
|
+
)
|
|
215
|
+
throw new TypeError("Invalid file selection");
|
|
216
|
+
const { rows } = reportFileRows(report, context);
|
|
217
|
+
const matches = rows.filter((file) =>
|
|
218
|
+
keys[0] === "fileId"
|
|
219
|
+
? file.fileId === selection.fileId
|
|
220
|
+
: keys[0] === "path"
|
|
221
|
+
? file.path === selection.path
|
|
222
|
+
: file.path === null && file.originalPath === selection.originalPath,
|
|
223
|
+
);
|
|
224
|
+
if (matches.length !== 1) return null;
|
|
225
|
+
const file = matches[0];
|
|
226
|
+
const rawFile = (report.files ?? []).find(
|
|
227
|
+
(item) =>
|
|
228
|
+
item.path === file.path && item.originalPath === file.originalPath,
|
|
229
|
+
);
|
|
230
|
+
const samePathEvidence = (report.evidence ?? []).filter((item) =>
|
|
231
|
+
file.path === null
|
|
232
|
+
? item.side === "artifact" &&
|
|
233
|
+
item.path === null &&
|
|
234
|
+
item.originalPath === file.originalPath
|
|
235
|
+
: item.path === file.path &&
|
|
236
|
+
(file.state !== "removed" || item.side === "source"),
|
|
237
|
+
);
|
|
238
|
+
if (file.state === "removed") {
|
|
239
|
+
const hashes = new Set(
|
|
240
|
+
samePathEvidence
|
|
241
|
+
.filter(
|
|
242
|
+
(item) =>
|
|
243
|
+
digest(item.fileSha256) &&
|
|
244
|
+
digest(item.blobDigest) &&
|
|
245
|
+
item.blobDigest === report.inputs?.sourceArchive?.sha256,
|
|
246
|
+
)
|
|
247
|
+
.map((item) => item.fileSha256),
|
|
248
|
+
);
|
|
249
|
+
if (hashes.size === 1) file.sourceSha256 = [...hashes][0];
|
|
250
|
+
}
|
|
251
|
+
const candidateIds = new Set([
|
|
252
|
+
...(rawFile?.evidenceIds ?? []),
|
|
253
|
+
...samePathEvidence.map((item) => item.id),
|
|
254
|
+
]);
|
|
255
|
+
const result = {
|
|
256
|
+
schemaVersion: "1",
|
|
257
|
+
scope: "artifact-source",
|
|
258
|
+
file,
|
|
259
|
+
recordedOnly: true,
|
|
260
|
+
evidence: [],
|
|
261
|
+
evidenceStatus: "unavailable",
|
|
262
|
+
evidenceOmitted: 0,
|
|
263
|
+
invalidEvidenceOmitted: 0,
|
|
264
|
+
limitations: [
|
|
265
|
+
...explorationLimitations,
|
|
266
|
+
"Only previously recorded excerpts are returned; omitted text is not fetched or reconstructed. Hashes are recorded claims, not new artifact verification. Render excerpts as text, never HTML.",
|
|
267
|
+
],
|
|
268
|
+
};
|
|
269
|
+
const evidenceById = new Map();
|
|
270
|
+
for (const item of report.evidence ?? [])
|
|
271
|
+
evidenceById.set(item.id, evidenceById.has(item.id) ? null : item);
|
|
272
|
+
let bytes = Buffer.byteLength(explorationJson(result));
|
|
273
|
+
for (const id of candidateIds) {
|
|
274
|
+
const item = evidenceById.get(id);
|
|
275
|
+
const expectedHash =
|
|
276
|
+
item?.side === "artifact" ? file.artifactSha256 : file.sourceSha256;
|
|
277
|
+
const input =
|
|
278
|
+
item?.side === "artifact"
|
|
279
|
+
? report.inputs?.artifact
|
|
280
|
+
: report.inputs?.sourceArchive;
|
|
281
|
+
if (
|
|
282
|
+
!item ||
|
|
283
|
+
!/^ev-[a-f0-9]{64}$/.test(id) ||
|
|
284
|
+
!["artifact", "source"].includes(item.side) ||
|
|
285
|
+
item.path !== file.path ||
|
|
286
|
+
!safeExplorationPath(item.originalPath) ||
|
|
287
|
+
(item.side === "artifact" &&
|
|
288
|
+
(item.originalPath !== file.originalPath || item.size !== file.size)) ||
|
|
289
|
+
!expectedHash ||
|
|
290
|
+
item.fileSha256 !== expectedHash ||
|
|
291
|
+
!digest(item.blobDigest) ||
|
|
292
|
+
item.blobDigest !== input?.sha256 ||
|
|
293
|
+
!Number.isSafeInteger(item.size) ||
|
|
294
|
+
item.size < 0 ||
|
|
295
|
+
!validRange(item.range, item.size)
|
|
296
|
+
) {
|
|
297
|
+
result.invalidEvidenceOmitted++;
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
const evidence = {
|
|
301
|
+
id,
|
|
302
|
+
side: item.side,
|
|
303
|
+
path: item.path,
|
|
304
|
+
originalPath: item.originalPath,
|
|
305
|
+
fileSha256: item.fileSha256,
|
|
306
|
+
blobDigest: item.blobDigest,
|
|
307
|
+
size: item.size,
|
|
308
|
+
range: item.range,
|
|
309
|
+
...recordedExcerpt(item),
|
|
310
|
+
};
|
|
311
|
+
const size = Buffer.byteLength(explorationJson(evidence)) + 1;
|
|
312
|
+
if (
|
|
313
|
+
result.evidence.length >= 32 ||
|
|
314
|
+
bytes + size > EXPLORATION_BYTE_LIMIT - 2048
|
|
315
|
+
) {
|
|
316
|
+
result.evidenceOmitted++;
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
bytes += size;
|
|
320
|
+
result.evidence.push(evidence);
|
|
321
|
+
}
|
|
322
|
+
if (result.evidence.length)
|
|
323
|
+
result.evidenceStatus =
|
|
324
|
+
result.evidenceOmitted || result.invalidEvidenceOmitted
|
|
325
|
+
? "partial"
|
|
326
|
+
: "recorded";
|
|
327
|
+
return result;
|
|
328
|
+
}
|
package/dist/server/app.mjs
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { createServer } from "node:http";
|
|
2
|
+
import { createRegistryFactsHandler } from "./registry-facts.mjs";
|
|
2
3
|
import { buildPackageDetails } from "../core/package-details.mjs";
|
|
3
4
|
import { attachVulnerabilityCheck } from "./enrichment.mjs";
|
|
4
5
|
import { createQueue, resolveLimits, systemClock } from "./queue.mjs";
|
|
5
6
|
import { createReportStore } from "./store.mjs";
|
|
7
|
+
import { handleReportExploration } from "./report-exploration.mjs";
|
|
6
8
|
import { renderReport } from "../viewer/render.mjs";
|
|
7
9
|
import {
|
|
8
10
|
renderHome,
|
|
@@ -105,6 +107,7 @@ export function createApp({
|
|
|
105
107
|
versionInfo,
|
|
106
108
|
limits: overrides,
|
|
107
109
|
clock = systemClock,
|
|
110
|
+
registryFacts = {},
|
|
108
111
|
} = {}) {
|
|
109
112
|
if (typeof runAnalysis !== "function")
|
|
110
113
|
throw new TypeError("runAnalysis is required");
|
|
@@ -119,6 +122,7 @@ export function createApp({
|
|
|
119
122
|
);
|
|
120
123
|
if (enrichJob !== undefined && typeof enrichJob !== "function")
|
|
121
124
|
throw new TypeError("enrichJob must be a function");
|
|
125
|
+
const registryFactsHandler = createRegistryFactsHandler(registryFacts);
|
|
122
126
|
const limits = resolveLimits(overrides);
|
|
123
127
|
const version = Object.fromEntries(
|
|
124
128
|
["version", "commit", "tarballSha256"].map((key) => [
|
|
@@ -174,6 +178,7 @@ export function createApp({
|
|
|
174
178
|
}
|
|
175
179
|
async function handle() {
|
|
176
180
|
const path = req.url;
|
|
181
|
+
if (await registryFactsHandler(req, json)) return;
|
|
177
182
|
if (path === "/check") {
|
|
178
183
|
if (req.method !== "POST")
|
|
179
184
|
return error(
|
|
@@ -257,6 +262,7 @@ export function createApp({
|
|
|
257
262
|
"Use the release form to start a check.",
|
|
258
263
|
{ allow: "GET" },
|
|
259
264
|
);
|
|
265
|
+
if (handleReportExploration({ path, getJob: queue.get, send })) return;
|
|
260
266
|
if (path === "/") return send(200, renderHome());
|
|
261
267
|
if (path === "/healthz") return json(200, { status: "ok" });
|
|
262
268
|
if (path === "/version") return json(200, version);
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildReportFileInventory,
|
|
3
|
+
buildReportFileEvidence,
|
|
4
|
+
explorationJson,
|
|
5
|
+
} from "../core/report-exploration.mjs";
|
|
6
|
+
|
|
7
|
+
function queryParameters(raw, allowed) {
|
|
8
|
+
if (raw.length > 16384) throw new TypeError("Query too long");
|
|
9
|
+
decodeURIComponent(raw.replace(/\+/g, " "));
|
|
10
|
+
const params = new URLSearchParams(raw);
|
|
11
|
+
for (const key of params.keys())
|
|
12
|
+
if (!allowed.includes(key) || params.getAll(key).length !== 1)
|
|
13
|
+
throw new TypeError("Unknown or duplicate query key");
|
|
14
|
+
return params;
|
|
15
|
+
}
|
|
16
|
+
function integer(params, name, fallback) {
|
|
17
|
+
if (!params.has(name)) return fallback;
|
|
18
|
+
const value = params.get(name);
|
|
19
|
+
if (!/^(0|[1-9]\d*)$/.test(value)) throw new TypeError("Invalid integer");
|
|
20
|
+
return Number(value);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function handleReportExploration({ path, getJob, send }) {
|
|
24
|
+
const match = /^\/r\/([a-f0-9]{32})\/(files|file)\.json(?:\?([^#]*))?$/.exec(
|
|
25
|
+
path,
|
|
26
|
+
);
|
|
27
|
+
if (!match) return false;
|
|
28
|
+
const json = (status, body, headers = {}) =>
|
|
29
|
+
send(status, explorationJson(body), "application/json; charset=utf-8", {
|
|
30
|
+
"content-disposition":
|
|
31
|
+
match[2] === "file"
|
|
32
|
+
? 'attachment; filename="unmask-file-evidence.json"'
|
|
33
|
+
: 'attachment; filename="unmask-files.json"',
|
|
34
|
+
...headers,
|
|
35
|
+
});
|
|
36
|
+
const drilldown = match[2] === "file";
|
|
37
|
+
const context = { reportId: match[1] };
|
|
38
|
+
let options;
|
|
39
|
+
try {
|
|
40
|
+
if (drilldown) {
|
|
41
|
+
const params = queryParameters(match[3] ?? "", [
|
|
42
|
+
"path",
|
|
43
|
+
"originalPath",
|
|
44
|
+
"fileId",
|
|
45
|
+
]);
|
|
46
|
+
options = Object.fromEntries(params);
|
|
47
|
+
buildReportFileEvidence({ files: [] }, options, context);
|
|
48
|
+
} else {
|
|
49
|
+
const params = queryParameters(match[3] ?? "", [
|
|
50
|
+
"state",
|
|
51
|
+
"prefix",
|
|
52
|
+
"offset",
|
|
53
|
+
"limit",
|
|
54
|
+
]);
|
|
55
|
+
options = {
|
|
56
|
+
state: params.get("state") ?? "all",
|
|
57
|
+
prefix: params.get("prefix") ?? "",
|
|
58
|
+
offset: integer(params, "offset", 0),
|
|
59
|
+
limit: integer(params, "limit", 50),
|
|
60
|
+
};
|
|
61
|
+
buildReportFileInventory({ files: [] }, options, context);
|
|
62
|
+
}
|
|
63
|
+
} catch {
|
|
64
|
+
json(400, { error: "invalid-query" });
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
const job = getJob(match[1]);
|
|
68
|
+
if (!job) json(404, { error: "report-unavailable" });
|
|
69
|
+
else if (!job.report) {
|
|
70
|
+
const pending = ["queued", "running"].includes(job.status);
|
|
71
|
+
json(
|
|
72
|
+
pending ? 202 : 409,
|
|
73
|
+
{
|
|
74
|
+
error: pending ? "report-pending" : "report-unavailable",
|
|
75
|
+
status: job.status,
|
|
76
|
+
},
|
|
77
|
+
pending ? { "retry-after": "3" } : {},
|
|
78
|
+
);
|
|
79
|
+
} else {
|
|
80
|
+
const body = drilldown
|
|
81
|
+
? buildReportFileEvidence(job.report, options, context)
|
|
82
|
+
: buildReportFileInventory(job.report, options, context);
|
|
83
|
+
if (!body) json(404, { error: "file-unavailable" });
|
|
84
|
+
else json(200, { reportId: match[1], ...body });
|
|
85
|
+
}
|
|
86
|
+
return true;
|
|
87
|
+
}
|
package/dist/version.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"buildSchema":"1","version":"0.1.
|
|
1
|
+
{"buildSchema":"1","version":"0.1.3","dirty":false,"commit":"541bc4d2e4710b411202882ec2480801994b6cf9","sourceSha256":"429745022e980eb8d09b1c6a6614b8b25f49921bd1883b0b111dfd5c3137ada3","packageSha256":"28210403766aff6ca070f8ca93df42117e93eedb5005b2a1b1fec4ee2f68c0c0","lockfileSha256":"2f632db652163f9f8f55ed649962fc5e4f718daf2edf6b0f0eea5d9501013dac","files":{"cli/batch.mjs":{"sha256":"101f7ecb4c3d5890c42f402e5ca00ab97351c4a4d90e07a1fa4367e3c0807650","size":7296},"cli/hosted.mjs":{"sha256":"dea7b512a2e811102521565e9f218c77aab916fe05e838f55a9aeb7b43cd0852","size":7436},"cli/main.mjs":{"sha256":"c4429dd331b377a1b1ca9b25a04721d51f38c39ee395de2239d0ad2bf7ccb57c","size":6825},"cli/scan.mjs":{"sha256":"9bf33e926ededf1beb1e4af73c861ba5bf33bee74ca0544151112284e0afc1b5","size":3906},"core/acquire.mjs":{"sha256":"aee5ed002292edf1b03385ceec8ebfb03de452dba36bbdced96f888eaafa1d67","size":9549},"core/advisories.mjs":{"sha256":"388088e44422ffeb3b60d930c84cec29fbc89b98d8dfd2879bc6ede50f4e7ea9","size":13004},"core/archive.mjs":{"sha256":"80aa4373c0324458fe322a83af18a4cfd0ffec4b39ebdeffc9084639a8655410","size":16193},"core/compare.mjs":{"sha256":"540614266e06a2875010793d036d256ddfac72f978157cd3d147ffb99015d8e4","size":7579},"core/manifests.mjs":{"sha256":"29c40e01ff1e1dfaed444600d8c78380d23d4bc9be71960251e39ea3594de2f1","size":17891},"core/online.mjs":{"sha256":"368a3f2558e16db0701d2eaf068b48e2661e7e01ab7cf5605cdd22766aeb5cca","size":9741},"core/package-details.mjs":{"sha256":"770551a6ab84e523dbc56ede6131b08eb22b8c21fc9c8b179fe0d1769fb2ec00","size":21538},"core/pipeline.mjs":{"sha256":"9c4ddc5c258a99455a1230c978fd132c135057ad7bb6c7baa201ae530ee136fd","size":11060},"core/policy.mjs":{"sha256":"4281c626c00a6424992f5e1eba26876e2b70f4b968450a8ca52fa42f5a121ba5","size":2954},"core/registry-facts.mjs":{"sha256":"726344597044b0c23fb74aff4fbfe7c60cbbfc4c0bee838abf9257134f9c96db","size":11013},"core/report-exploration.mjs":{"sha256":"1dcc9897d3bbc98ee3107ef8f8279d9eed6ee6d104455eb214a5e5644bd197fb","size":10137},"core/report.mjs":{"sha256":"aa167c94c257a5ff6d830630a37a51a9a9e47f82aa4a0a7b96ead91a5967294b","size":15844},"core/resolve.mjs":{"sha256":"d5575da49ff78e8fea8df1f2fc10a39540adea285eddfa347746e848f9fdf957","size":5202},"core/source.mjs":{"sha256":"798fe74e3fba836b82576cd46e1b91e6f6e0a4e3cf273c533b5e074247c81373","size":3238},"core/syntax.mjs":{"sha256":"2d50f2e73e17aedbcbccd92af280e171de4ef78b55acccd23d6b6ef11e891776","size":10450},"model/client.mjs":{"sha256":"efb68132710cbdff6b86b58cbac006c48e10f9317e852f00b120ef7c3c7ed9c1","size":42865},"model/config.mjs":{"sha256":"127605979a299616be15f4a47e94dcd82928b00ad2af71744ffaa6e359516486","size":3245},"server/app.mjs":{"sha256":"2aa5678bcdd38702707fa05ec7aa749d46f1590993e491ec0adc408a35d2f890","size":10498},"server/enrichment.mjs":{"sha256":"3c5cc57f2c8f019f79418457f366a9422e0a03a8ba86a41b0767aba636bde179","size":3825},"server/main.mjs":{"sha256":"e5bdcd02209c2ccc1ed8c73b85b9988d61aa3e1bcf7c8d119eed782a4bfcd224","size":4587},"server/queue.mjs":{"sha256":"23e30f3ff570771b77a244be9200ad510d2f19811eee566d68b364b7f741a9d7","size":7371},"server/registry-facts.mjs":{"sha256":"58bce5b2a40f25e7561f122ea012594c9eb6789f993ca83dadc07f198afca9e3","size":3315},"server/report-exploration.mjs":{"sha256":"862b8d0ea07b627368aaca3a0f66359cfc638b14c54ab4158f8ef313602c3810","size":2774},"server/runner.mjs":{"sha256":"a6ca5bfbd7b239f7110639d854ca9a263f4cf3aaf06701ea54493b29f8244706","size":9071},"server/store.mjs":{"sha256":"efa2a35c7e9d0344c26e24ba360a457cdf80f4f48975f0186f81844b9fb7a097","size":2706},"server/worker.mjs":{"sha256":"1df839608cd1c691fc2801d89e698daeef3e48d7f4b8843ce1252a21fde9730a","size":1902},"viewer/render.mjs":{"sha256":"c77442c27041a5fe28269b8c80c9d24c72b73a226e8d4491e6d7285a9feb5309","size":19443},"viewer/shell.mjs":{"sha256":"5f800d0a97fe416d02cf9abef766d651b444b4745ad3a998926599469358ca79","size":9408}}}
|