@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.
Files changed (50) hide show
  1. package/README.md +86 -4
  2. package/dist/cli/annotations.mjs +145 -0
  3. package/dist/cli/audit.mjs +223 -0
  4. package/dist/cli/cache.mjs +488 -0
  5. package/dist/cli/compare.mjs +261 -0
  6. package/dist/cli/config.mjs +141 -0
  7. package/dist/cli/discovery.mjs +372 -0
  8. package/dist/cli/doctor.mjs +354 -0
  9. package/dist/cli/exceptions.mjs +170 -0
  10. package/dist/cli/file.mjs +271 -0
  11. package/dist/cli/files.mjs +141 -0
  12. package/dist/cli/history.mjs +232 -0
  13. package/dist/cli/hosted.mjs +251 -43
  14. package/dist/cli/info.mjs +125 -0
  15. package/dist/cli/init.mjs +57 -0
  16. package/dist/cli/install-guard.mjs +896 -0
  17. package/dist/cli/lockfile.mjs +330 -0
  18. package/dist/cli/main.mjs +97 -14
  19. package/dist/cli/preflight.mjs +388 -0
  20. package/dist/cli/sarif.mjs +100 -0
  21. package/dist/cli/shell-integration.mjs +122 -0
  22. package/dist/cli/terminal.mjs +396 -0
  23. package/dist/cli/watch.mjs +233 -0
  24. package/dist/cli/why.mjs +256 -0
  25. package/dist/core/advisories.mjs +101 -24
  26. package/dist/core/advisory-cache.mjs +295 -0
  27. package/dist/core/behavior-signals.mjs +1088 -0
  28. package/dist/core/dependency-graph.mjs +332 -0
  29. package/dist/core/evidence-exports.mjs +469 -0
  30. package/dist/core/guard-config.mjs +463 -0
  31. package/dist/core/guard-policy.mjs +308 -0
  32. package/dist/core/package-details.mjs +42 -6
  33. package/dist/core/report.mjs +7 -0
  34. package/dist/core/sarif.mjs +556 -0
  35. package/dist/core/syntax.mjs +296 -15
  36. package/dist/model/client.mjs +204 -56
  37. package/dist/model/diff-evidence.mjs +177 -0
  38. package/dist/model/summary.mjs +183 -0
  39. package/dist/server/advisory-result.mjs +137 -0
  40. package/dist/server/app.mjs +60 -2
  41. package/dist/server/dependency-screening.mjs +386 -0
  42. package/dist/server/exports.mjs +63 -0
  43. package/dist/server/guard-decision.mjs +37 -0
  44. package/dist/server/main.mjs +25 -7
  45. package/dist/server/queue.mjs +95 -53
  46. package/dist/server/store.mjs +21 -1
  47. package/dist/version.json +1 -1
  48. package/dist/viewer/render.mjs +10 -2
  49. package/dist/viewer/shell.mjs +1 -1
  50. package/package.json +1 -1
@@ -3,7 +3,8 @@ import { parsePackageSpec } from "../core/acquire.mjs";
3
3
  import { LocalInputError } from "../core/pipeline.mjs";
4
4
  import { validateReport } from "../core/report.mjs";
5
5
 
6
- const DEFAULT_SERVER_URL = "https://unmask-386156642190.asia-south1.run.app";
6
+ export const DEFAULT_SERVER_URL =
7
+ "https://unmask-386156642190.asia-south1.run.app";
7
8
  const defaults = Object.freeze({
8
9
  totalTimeoutMs: 600000,
9
10
  requestTimeoutMs: 15000,
@@ -11,7 +12,7 @@ const defaults = Object.freeze({
11
12
  pollMs: 3000,
12
13
  });
13
14
  const failure = (code, message) => new LocalInputError(code, message);
14
- function serverOrigin(value) {
15
+ export function serverOrigin(value) {
15
16
  if (!value)
16
17
  throw failure(
17
18
  "hosted-configuration",
@@ -88,17 +89,30 @@ async function readJson(response, maxBytes) {
88
89
  throw failure("hosted-response", "The hosted response is not valid JSON.");
89
90
  }
90
91
  }
91
- export async function checkHosted(
92
+ async function runHosted(
92
93
  spec,
93
94
  {
94
95
  serverUrl = DEFAULT_SERVER_URL,
95
96
  fetchImpl = globalThis.fetch,
96
97
  signal,
97
98
  limits: overrides = {},
99
+ existingReportId,
100
+ optionalDetails = false,
101
+ onStatus,
98
102
  } = {},
103
+ withDetails = false,
99
104
  ) {
100
105
  const claim = parsePackageSpec(spec);
101
106
  const origin = serverOrigin(serverUrl);
107
+ if (
108
+ existingReportId !== undefined &&
109
+ (typeof existingReportId !== "string" ||
110
+ !/^[a-f0-9]{32}$/.test(existingReportId))
111
+ )
112
+ throw failure(
113
+ "hosted-configuration",
114
+ "Use a recorded 32-character lowercase hexadecimal report ID.",
115
+ );
102
116
  const limits = { ...defaults, ...overrides };
103
117
  if (
104
118
  Object.keys(limits).some(
@@ -147,46 +161,54 @@ export async function checkHosted(
147
161
  if (response) await discard(response);
148
162
  }
149
163
  }
164
+ const status = (phase) => {
165
+ try {
166
+ onStatus?.(phase);
167
+ } catch {}
168
+ };
150
169
  try {
151
- const reportUrl = await request(
152
- origin + "/check",
153
- {
154
- method: "POST",
155
- headers: {
156
- "content-type": "application/x-www-form-urlencoded",
157
- accept: "application/json",
158
- },
159
- body: new URLSearchParams({ package: spec }),
160
- },
161
- async (response) => {
162
- if (response.status !== 303)
163
- throw failure(
164
- "hosted-submit-failed",
165
- `The hosted service did not accept the check (HTTP ${response.status}). No local fallback was attempted.`,
166
- );
167
- const location = response.headers.get("location");
168
- let url;
169
- try {
170
- url = new URL(location, origin);
171
- } catch {}
172
- if (
173
- !location ||
174
- location.length > 2048 ||
175
- !url ||
176
- url.origin !== origin ||
177
- url.username ||
178
- url.password ||
179
- url.search ||
180
- url.hash ||
181
- !/^\/r\/[a-f0-9]{32}$/.test(url.pathname)
182
- )
183
- throw failure(
184
- "hosted-response",
185
- "The hosted service returned an invalid report location.",
186
- );
187
- return url.href + ".json";
188
- },
189
- );
170
+ if (!existingReportId) status("submitting");
171
+ const reportUrl = existingReportId
172
+ ? `${origin}/r/${existingReportId}.json`
173
+ : await request(
174
+ origin + "/check",
175
+ {
176
+ method: "POST",
177
+ headers: {
178
+ "content-type": "application/x-www-form-urlencoded",
179
+ accept: "application/json",
180
+ },
181
+ body: new URLSearchParams({ package: spec }),
182
+ },
183
+ async (response) => {
184
+ if (response.status !== 303)
185
+ throw failure(
186
+ "hosted-submit-failed",
187
+ `The hosted service did not accept the check (HTTP ${response.status}). No local fallback was attempted.`,
188
+ );
189
+ const location = response.headers.get("location");
190
+ let url;
191
+ try {
192
+ url = new URL(location, origin);
193
+ } catch {}
194
+ if (
195
+ !location ||
196
+ location.length > 2048 ||
197
+ !url ||
198
+ url.origin !== origin ||
199
+ url.username ||
200
+ url.password ||
201
+ url.search ||
202
+ url.hash ||
203
+ !/^\/r\/[a-f0-9]{32}$/.test(url.pathname)
204
+ )
205
+ throw failure(
206
+ "hosted-response",
207
+ "The hosted service returned an invalid report location.",
208
+ );
209
+ return url.href + ".json";
210
+ },
211
+ );
190
212
  const id = new URL(reportUrl).pathname.slice(3, -5);
191
213
  for (;;) {
192
214
  const result = await request(
@@ -208,8 +230,14 @@ export async function checkHosted(
208
230
  "hosted-response",
209
231
  "The hosted service returned an invalid pending report.",
210
232
  );
233
+ status(value.status);
211
234
  return null;
212
235
  }
236
+ if (
237
+ ["failed", "incomplete"].includes(value?.status) &&
238
+ value.id === id
239
+ )
240
+ throw advisoryOnly(value.advisoryResult, claim);
213
241
  try {
214
242
  validateReport(value);
215
243
  if (
@@ -227,7 +255,34 @@ export async function checkHosted(
227
255
  return value;
228
256
  },
229
257
  );
230
- if (result) return result;
258
+ if (result) {
259
+ if (!withDetails) return result;
260
+ status("details");
261
+ const detailsUrl = reportUrl.replace(/\.json$/, ".details.json");
262
+ const fetchDetails = () =>
263
+ request(
264
+ detailsUrl,
265
+ { headers: { accept: "application/json" } },
266
+ async (response) => {
267
+ if (response.status !== 200)
268
+ throw failure(
269
+ "hosted-details-failed",
270
+ `Hosted package details are unavailable (HTTP ${response.status}).`,
271
+ );
272
+ const value = await readJson(
273
+ response,
274
+ Math.min(limits.maxBodyBytes, 4194304),
275
+ );
276
+ validateDetails(value, result);
277
+ return value;
278
+ },
279
+ );
280
+ // Check summaries keep a completed report when only details fail.
281
+ const details = optionalDetails
282
+ ? await fetchDetails().catch(() => null)
283
+ : await fetchDetails();
284
+ return { report: result, details, reportUrl, detailsUrl };
285
+ }
231
286
  await delay(limits.pollMs, undefined, { signal: lifetime });
232
287
  }
233
288
  } catch (error) {
@@ -249,3 +304,156 @@ export async function checkHosted(
249
304
  clearTimeout(totalTimer);
250
305
  }
251
306
  }
307
+
308
+ function validateDetails(value, report) {
309
+ const list = (entry) =>
310
+ entry &&
311
+ Array.isArray(entry.items) &&
312
+ entry.items.length <= 20000 &&
313
+ entry.items.every(
314
+ (item) =>
315
+ item !== null && typeof item === "object" && !Array.isArray(item),
316
+ ) &&
317
+ ["total", "returned", "omitted"].every(
318
+ (key) => Number.isSafeInteger(entry[key]) && entry[key] >= 0,
319
+ ) &&
320
+ entry.returned === entry.items.length &&
321
+ entry.total === entry.returned + entry.omitted;
322
+ const identity = value?.identity?.items?.[0];
323
+ if (
324
+ value?.detailsVersion !== "1" ||
325
+ value?.reportSchemaVersion !== report.schemaVersion ||
326
+ !list(value.identity) ||
327
+ identity?.requested?.name !== report.inputs.claim.name ||
328
+ identity?.requested?.version !== report.inputs.claim.version ||
329
+ identity?.artifactDigests?.sha256 !== report.inputs.artifact.sha256 ||
330
+ !list(value.declarations?.fields) ||
331
+ !list(value.scripts?.entries) ||
332
+ !Array.isArray(value.dependencies?.categories) ||
333
+ value.dependencies.categories.length > 6 ||
334
+ value.dependencies.categories.some(
335
+ (category) => !list(category?.entries),
336
+ ) ||
337
+ !list(value.provenance?.facts) ||
338
+ !list(value.inventory?.files)
339
+ )
340
+ throw failure(
341
+ "hosted-details-invalid",
342
+ "Hosted details failed version, identity or inventory validation.",
343
+ );
344
+ const check = value.vulnerabilityCheck;
345
+ if (
346
+ check &&
347
+ (check.source !== "OSV" ||
348
+ ![
349
+ "found",
350
+ "none-reported",
351
+ "incomplete",
352
+ "unavailable",
353
+ "not-assessed",
354
+ ].includes(check.status) ||
355
+ typeof check.complete !== "boolean" ||
356
+ !Array.isArray(check.advisories) ||
357
+ check.complete !== ["found", "none-reported"].includes(check.status) ||
358
+ (check.status === "found" && check.advisories.length === 0) ||
359
+ (check.status === "none-reported" && check.advisories.length !== 0) ||
360
+ (check.package &&
361
+ (check.package.ecosystem !== "npm" ||
362
+ check.package.name !== report.inputs.claim.name ||
363
+ check.package.version !== report.inputs.claim.version)) ||
364
+ (check.complete &&
365
+ (!check.package || !["found", "none-reported"].includes(check.status))))
366
+ )
367
+ throw failure(
368
+ "hosted-details-invalid",
369
+ "Hosted OSV details failed status or identity validation.",
370
+ );
371
+ }
372
+
373
+ const ADVISORY_ASSESSMENTS = {
374
+ found: "advisories-reported",
375
+ "none-reported": "no-advisory-reported",
376
+ incomplete: "advisory-lookup-incomplete",
377
+ unavailable: "advisory-lookup-unavailable",
378
+ };
379
+ const bounded = (value, max, optional = false) =>
380
+ (optional && value === undefined) ||
381
+ (typeof value === "string" && Buffer.byteLength(value) <= max);
382
+ const strings = (value, count, max) =>
383
+ Array.isArray(value) &&
384
+ value.length <= count &&
385
+ value.every((item) => bounded(item, max));
386
+ function advisoryRecord(item) {
387
+ return (
388
+ item !== null &&
389
+ typeof item === "object" &&
390
+ !Array.isArray(item) &&
391
+ typeof item.id === "string" &&
392
+ /^[a-zA-Z0-9:_.-]{1,256}$/.test(item.id) &&
393
+ bounded(item.summary, 2048, true) &&
394
+ bounded(item.databaseSeverity, 64, true) &&
395
+ (item.aliases === undefined || strings(item.aliases, 16, 256))
396
+ );
397
+ }
398
+ function advisoryOnly(result, claim) {
399
+ const check = result?.vulnerabilityCheck;
400
+ const exact = (item) =>
401
+ item?.ecosystem === "npm" &&
402
+ item.name === claim.name &&
403
+ item.version === claim.version;
404
+ const complete = ["found", "none-reported"].includes(check?.status);
405
+ if (
406
+ result?.schemaVersion !== "1" ||
407
+ result.kind !== "advisory-only" ||
408
+ result.safetyVerdict !== "not-provided" ||
409
+ result.artifactVerification?.status !== "not-performed" ||
410
+ result.sourceComparison?.status !== "not-performed" ||
411
+ !bounded(result.artifactVerification.reason, 128, true) ||
412
+ !bounded(result.notice, 1024) ||
413
+ !exact(result.package) ||
414
+ !exact(check?.package) ||
415
+ check.schemaVersion !== "1" ||
416
+ check.source !== "OSV" ||
417
+ !bounded(check.queriedAt, 64) ||
418
+ !Number.isFinite(Date.parse(check.queriedAt)) ||
419
+ !Number.isSafeInteger(check.pagesFetched) ||
420
+ check.pagesFetched < 0 ||
421
+ check.pagesFetched > 100 ||
422
+ (complete && (check.pagesFetched === 0 || check.reasons.length !== 0)) ||
423
+ (!complete && check.reasons.length === 0) ||
424
+ !Object.hasOwn(ADVISORY_ASSESSMENTS, check.status) ||
425
+ result.assessment !== ADVISORY_ASSESSMENTS[check.status] ||
426
+ check.complete !== complete ||
427
+ !strings(check.reasons, 16, 128) ||
428
+ !Array.isArray(check.advisories) ||
429
+ check.advisories.length > 100 ||
430
+ !check.advisories.every(advisoryRecord) ||
431
+ result.advisoryCount !== check.advisories.length ||
432
+ (check.status === "found" && check.advisories.length === 0) ||
433
+ (check.status === "none-reported" && check.advisories.length !== 0)
434
+ )
435
+ return failure(
436
+ "hosted-report-failed",
437
+ "The hosted analysis did not finish and returned no valid advisory-only result. No local fallback was attempted.",
438
+ );
439
+ const error = failure(
440
+ "hosted-advisory-only",
441
+ "The registry artifact could not be analyzed; only exact-version advisories are available. No verdict is claimed.",
442
+ );
443
+ error.advisoryResult = result;
444
+ return error;
445
+ }
446
+
447
+ export async function checkHosted(spec, options) {
448
+ return runHosted(spec, options);
449
+ }
450
+
451
+ export async function getHostedDetails(spec, options) {
452
+ return runHosted(spec, options, true);
453
+ }
454
+
455
+ export async function getHostedDetailsForReport(spec, reportId, options = {}) {
456
+ if (reportId === undefined)
457
+ throw failure("hosted-configuration", "An existing report ID is required.");
458
+ return runHosted(spec, { ...options, existingReportId: reportId }, true);
459
+ }
@@ -0,0 +1,125 @@
1
+ import { parsePackageSpec, AcquisitionError } from "../core/acquire.mjs";
2
+ import { LocalInputError } from "../core/pipeline.mjs";
3
+ import { getHostedDetails } from "./hosted.mjs";
4
+
5
+ export function terminalText(value, max = 2048) {
6
+ const text =
7
+ typeof value === "string" ? value : (JSON.stringify(value) ?? "unknown");
8
+ const escaped = text.replace(
9
+ /[\x00-\x1f\x7f-\x9f\u061c\u200b-\u200f\u2028-\u202e\u2060\u2066-\u2069\ufeff]/g,
10
+ (char) => "\\u" + char.charCodeAt(0).toString(16).padStart(4, "0"),
11
+ );
12
+ return escaped.length > max
13
+ ? escaped.slice(0, max) + "… [truncated]"
14
+ : escaped;
15
+ }
16
+
17
+ export function aiLines(model) {
18
+ const lines = [
19
+ `AI: ${terminalText(model.status)}${model.reason ? " (" + terminalText(model.reason) + ")" : ""}`,
20
+ ];
21
+ if (
22
+ model.status === "complete" &&
23
+ typeof model.summary === "string" &&
24
+ model.summary.trim()
25
+ )
26
+ lines.push(`AI summary: ${terminalText(model.summary)}`);
27
+ return lines;
28
+ }
29
+
30
+ export function exactPackage(spec) {
31
+ try {
32
+ return parsePackageSpec(spec);
33
+ } catch (error) {
34
+ if (error instanceof AcquisitionError)
35
+ throw new LocalInputError(error.code, error.message);
36
+ throw error;
37
+ }
38
+ }
39
+
40
+ export function hostedOptions(runtime) {
41
+ return {
42
+ serverUrl: (runtime.env ?? process.env).UNMASK_SERVER_URL,
43
+ fetchImpl: runtime.fetchImpl,
44
+ signal: runtime.signal,
45
+ limits: runtime.hostedLimits,
46
+ };
47
+ }
48
+
49
+ function rows(list, render, max = 100) {
50
+ const items = list?.items ?? [];
51
+ const lines = items.slice(0, max).map(render);
52
+ const omitted = (list?.omitted ?? 0) + Math.max(0, items.length - max);
53
+ if (omitted)
54
+ lines.push(` ${omitted} entries omitted by hosted/terminal limits.`);
55
+ return lines;
56
+ }
57
+
58
+ export function renderInfo({ details, report, reportUrl }) {
59
+ const claim = report.inputs.claim;
60
+ const text = terminalText;
61
+ const lines = [
62
+ `UNMASK info ${text(claim.name)}@${text(claim.version)}`,
63
+ "Publisher declarations (runtime behavior unverified):",
64
+ ...rows(
65
+ details.declarations.fields,
66
+ (item) =>
67
+ ` ${text(item.field)}: ${text(item.status === "declared" ? item.value : item.status)}`,
68
+ ),
69
+ "Scripts (never executed):",
70
+ ` Status: ${text(details.scripts.status)}`,
71
+ ...rows(
72
+ details.scripts.entries,
73
+ (item) =>
74
+ ` ${text(item.name)}: ${text(item.value)}${item.installHook ? " [install hook]" : ""}`,
75
+ ),
76
+ "Dependencies (declared only; transitive dependencies not assessed):",
77
+ ];
78
+ for (const category of details.dependencies.categories)
79
+ lines.push(
80
+ ` ${text(category.field)}: ${text(category.status)}; declared=${text(category.declaredCount)}`,
81
+ ...rows(
82
+ category.entries,
83
+ (item) =>
84
+ ` ${text(item.name)}${Object.hasOwn(item, "value") ? ": " + text(item.value) : ""} [${text(item.status)}]`,
85
+ ),
86
+ );
87
+ lines.push(
88
+ "Provenance (report-recorded verification status):",
89
+ ...rows(
90
+ details.provenance.facts,
91
+ (item) => ` ${text(item.field)}: ${text(item.value)}`,
92
+ ),
93
+ );
94
+ const check = details.vulnerabilityCheck;
95
+ lines.push(
96
+ `OSV exact-package query: ${text(check?.status ?? "not-assessed")}; complete=${check?.complete === true}`,
97
+ );
98
+ if (check?.queriedAt) lines.push(` Queried at: ${text(check.queriedAt)}`);
99
+ if (check?.reasons?.length) lines.push(` Reasons: ${text(check.reasons)}`);
100
+ const advisories = check?.advisories ?? [];
101
+ for (const item of advisories.slice(0, 100))
102
+ lines.push(
103
+ ` ${text(item?.id)}: ${text(item?.summary ?? "No summary recorded")}`,
104
+ );
105
+ if (advisories.length > 100)
106
+ lines.push(
107
+ ` ${advisories.length - 100} advisory records omitted from terminal output.`,
108
+ );
109
+ lines.push(
110
+ ...aiLines(report.model),
111
+ `Inventory complete: ${details.inventory.complete === true}; coverage complete: ${report.coverage.complete === true}`,
112
+ "This is not a safety verdict. No findings never means safe; missing or omitted facts remain unassessed.",
113
+ `Report: ${text(reportUrl)}`,
114
+ );
115
+ return lines.join("\n") + "\n";
116
+ }
117
+
118
+ export async function infoCommand(args, runtime = {}) {
119
+ if (args.length !== 1)
120
+ throw new LocalInputError("usage", "Use info name@exact-version");
121
+ exactPackage(args[0]);
122
+ const result = await getHostedDetails(args[0], hostedOptions(runtime));
123
+ (runtime.stdout ?? process.stdout).write(renderInfo(result));
124
+ return 0;
125
+ }
@@ -0,0 +1,57 @@
1
+ import {
2
+ DEFAULT_PROFILE,
3
+ GUARD_CONFIG_SCHEMA,
4
+ GuardConfigError,
5
+ createGuardConfig,
6
+ loadGuardSettings,
7
+ validateSetting,
8
+ } from "../core/guard-config.mjs";
9
+ import { parseCommandOptions } from "./config.mjs";
10
+
11
+ const USAGE =
12
+ "Use init [--profile strict|balanced|audit] [--project DIR]; it only creates a new .unmask.json and never installs or runs anything";
13
+
14
+ export async function runInit(args, runtime = {}) {
15
+ const stdout = runtime.stdout ?? process.stdout;
16
+ const stderr = runtime.stderr ?? process.stderr;
17
+ const now = runtime.now ? runtime.now() : new Date();
18
+ try {
19
+ const { positional, options } = parseCommandOptions(
20
+ args,
21
+ [],
22
+ ["--project", "--profile"],
23
+ );
24
+ if (positional.length) throw new GuardConfigError("usage", USAGE);
25
+ const profile = validateSetting(
26
+ "profile",
27
+ options.profile ?? DEFAULT_PROFILE,
28
+ );
29
+ const project = options.project ?? runtime.cwd ?? process.cwd();
30
+ const created = await createGuardConfig(
31
+ project,
32
+ { schemaVersion: GUARD_CONFIG_SCHEMA, profile },
33
+ { now },
34
+ );
35
+ const loaded = await loadGuardSettings(project, { now });
36
+ if (loaded.source !== "file" || loaded.settings.profile !== profile)
37
+ throw new GuardConfigError(
38
+ "verify-failed",
39
+ "The created configuration did not read back as written",
40
+ );
41
+ stdout.write(
42
+ `Created ${created.file} with profile ${profile} (enforcement ${loaded.settings.enforcement}).\nNext: unmask config show; add exact expiring exceptions only with unmask exceptions add.\n`,
43
+ );
44
+ for (const warning of loaded.warnings)
45
+ stderr.write(`warning: ${warning}\n`);
46
+ return 0;
47
+ } catch (error) {
48
+ if (error instanceof GuardConfigError) {
49
+ stderr.write(`${error.code}: ${error.message}\n`);
50
+ return 2;
51
+ }
52
+ stderr.write(
53
+ "init-failed: Guard configuration could not be created; no file is claimed.\n",
54
+ );
55
+ return 1;
56
+ }
57
+ }