@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
@@ -0,0 +1,354 @@
1
+ import { constants } from "node:fs";
2
+ import { lstat, open } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { LocalInputError } from "../core/pipeline.mjs";
5
+
6
+ const DEFAULT_SERVER_URL = "https://unmask-386156642190.asia-south1.run.app";
7
+ const MAX_JSON_BYTES = 16384;
8
+ const fail = () =>
9
+ new LocalInputError(
10
+ "doctor-usage",
11
+ "Use doctor [--offline] [--server-url ORIGIN] [--project DIR] [--json].",
12
+ );
13
+ function parse(argv) {
14
+ const result = {};
15
+ for (let index = 0; index < argv.length; index++) {
16
+ const key = argv[index];
17
+ if (
18
+ !["--offline", "--json", "--server-url", "--project"].includes(key) ||
19
+ Object.hasOwn(result, key)
20
+ )
21
+ throw fail();
22
+ if (["--offline", "--json"].includes(key)) result[key] = true;
23
+ else {
24
+ const value = argv[++index];
25
+ if (!value || value.startsWith("--")) throw fail();
26
+ result[key] = value;
27
+ }
28
+ }
29
+ if (result["--offline"] && result["--server-url"]) throw fail();
30
+ return result;
31
+ }
32
+ function serviceOrigin(value) {
33
+ let url;
34
+ try {
35
+ url = new URL(value);
36
+ } catch {
37
+ throw fail();
38
+ }
39
+ if (
40
+ value.length > 2048 ||
41
+ url.username ||
42
+ url.password ||
43
+ url.pathname !== "/" ||
44
+ url.search ||
45
+ url.hash ||
46
+ !(
47
+ url.protocol === "https:" ||
48
+ (url.protocol === "http:" &&
49
+ ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname))
50
+ )
51
+ )
52
+ throw fail();
53
+ return url.origin;
54
+ }
55
+ async function serviceJson(
56
+ origin,
57
+ route,
58
+ { fetchImpl = globalThis.fetch, signal } = {},
59
+ ) {
60
+ const controller = new AbortController();
61
+ const timer = setTimeout(() => controller.abort(), 5000);
62
+ let response;
63
+ try {
64
+ response = await fetchImpl(origin + route, {
65
+ headers: { accept: "application/json" },
66
+ redirect: "manual",
67
+ credentials: "omit",
68
+ referrerPolicy: "no-referrer",
69
+ signal: signal
70
+ ? AbortSignal.any([signal, controller.signal])
71
+ : controller.signal,
72
+ });
73
+ if (
74
+ response.status !== 200 ||
75
+ response.redirected ||
76
+ (response.url && new URL(response.url).origin !== origin) ||
77
+ !/^application\/json(?:\s*;\s*charset=utf-8)?$/i.test(
78
+ response.headers.get("content-type") ?? "",
79
+ )
80
+ )
81
+ throw new Error();
82
+ const declared = response.headers.get("content-length");
83
+ if (
84
+ declared !== null &&
85
+ (!/^\d+$/.test(declared) || Number(declared) > MAX_JSON_BYTES)
86
+ )
87
+ throw new Error();
88
+ let size = 0;
89
+ const chunks = [];
90
+ for await (const chunk of response.body ?? []) {
91
+ size += chunk.byteLength;
92
+ if (size > MAX_JSON_BYTES) throw new Error();
93
+ chunks.push(chunk);
94
+ }
95
+ return JSON.parse(
96
+ new TextDecoder("utf-8", { fatal: true }).decode(
97
+ Buffer.concat(chunks, size),
98
+ ),
99
+ );
100
+ } finally {
101
+ clearTimeout(timer);
102
+ try {
103
+ await response?.body?.cancel();
104
+ } catch {}
105
+ }
106
+ }
107
+ async function fallbackConfig(cwd) {
108
+ try {
109
+ if (!(await lstat(cwd)).isDirectory()) throw new Error();
110
+ } catch {
111
+ return {
112
+ id: "config",
113
+ status: "error",
114
+ message: "Project directory does not exist or is not a directory.",
115
+ action: "Use --project with an existing project directory.",
116
+ };
117
+ }
118
+ const filename = path.join(cwd, ".unmask.json");
119
+ let handle;
120
+ try {
121
+ const stat = await lstat(filename);
122
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size > MAX_JSON_BYTES)
123
+ throw new Error();
124
+ handle = await open(filename, constants.O_RDONLY | constants.O_NOFOLLOW);
125
+ const current = await handle.stat();
126
+ if (
127
+ current.dev !== stat.dev ||
128
+ current.ino !== stat.ino ||
129
+ current.size > MAX_JSON_BYTES
130
+ )
131
+ throw new Error();
132
+ const bytes = Buffer.alloc(MAX_JSON_BYTES + 1);
133
+ const { bytesRead } = await handle.read(bytes, 0, bytes.length, 0);
134
+ if (bytesRead > MAX_JSON_BYTES) throw new Error();
135
+ const value = JSON.parse(
136
+ new TextDecoder("utf-8", { fatal: true }).decode(
137
+ bytes.subarray(0, bytesRead),
138
+ ),
139
+ );
140
+ if (
141
+ !value ||
142
+ Array.isArray(value) ||
143
+ !["strict", "balanced", "audit"].includes(value.profile)
144
+ )
145
+ throw new Error();
146
+ return {
147
+ id: "config",
148
+ status: "unknown",
149
+ message:
150
+ "Project config has a recognized profile; full guard schema validation requires the guard configuration command.",
151
+ action:
152
+ "Run unmask config show to validate the complete policy before enabling an install guard.",
153
+ };
154
+ } catch (error) {
155
+ if (error?.code === "ENOENT")
156
+ return {
157
+ id: "config",
158
+ status: "warning",
159
+ message: "No project guard configuration found.",
160
+ action: "Use unmask config to explicitly configure a guard policy.",
161
+ };
162
+ return {
163
+ id: "config",
164
+ status: "error",
165
+ message:
166
+ "Project guard configuration is unreadable, oversized, linked or invalid.",
167
+ action:
168
+ "Inspect the project guard config locally; do not store credentials in it.",
169
+ };
170
+ } finally {
171
+ await handle?.close();
172
+ }
173
+ }
174
+ async function projectConfig(cwd, inspector) {
175
+ if (inspector === null) return fallbackConfig(cwd);
176
+ try {
177
+ if (!inspector) {
178
+ try {
179
+ inspector = (await import("../core/guard-config.mjs"))
180
+ .inspectGuardConfig;
181
+ } catch (error) {
182
+ if (
183
+ error.code === "ERR_MODULE_NOT_FOUND" &&
184
+ error.url ===
185
+ new URL("../core/guard-config.mjs", import.meta.url).href
186
+ )
187
+ return fallbackConfig(cwd);
188
+ throw error;
189
+ }
190
+ }
191
+ const result = await inspector(cwd);
192
+ if (result?.status === "default")
193
+ return {
194
+ id: "config",
195
+ status: "warning",
196
+ message:
197
+ "No explicit project guard configuration; default policy applies.",
198
+ action:
199
+ "Use unmask config to explicitly inspect and choose a project guard policy.",
200
+ };
201
+ if (result?.status !== "ok") throw new Error();
202
+ return {
203
+ id: "config",
204
+ status: "ok",
205
+ message:
206
+ "Project guard configuration passed the installed guard schema validator.",
207
+ action:
208
+ "Run unmask config show to inspect the policy before enabling an install guard.",
209
+ };
210
+ } catch {
211
+ return {
212
+ id: "config",
213
+ status: "error",
214
+ message:
215
+ "Project guard configuration failed the installed guard schema validator.",
216
+ action:
217
+ "Inspect .unmask.json locally with unmask config; do not store credentials in it.",
218
+ };
219
+ }
220
+ }
221
+ export async function inspectDoctor({
222
+ cwd = process.cwd(),
223
+ nodeVersion = process.version,
224
+ offline = false,
225
+ serverUrl = DEFAULT_SERVER_URL,
226
+ fetchImpl,
227
+ signal,
228
+ configInspector,
229
+ } = {}) {
230
+ const origin = serviceOrigin(serverUrl);
231
+ const match = /^v?(\d+)\.(\d+)\.(\d+)$/.exec(nodeVersion);
232
+ const supported =
233
+ match &&
234
+ (Number(match[1]) > 24 ||
235
+ (Number(match[1]) === 24 &&
236
+ (Number(match[2]) > 22 ||
237
+ (Number(match[2]) === 22 && Number(match[3]) >= 2))));
238
+ const checks = [
239
+ {
240
+ id: "node",
241
+ status:
242
+ nodeVersion.replace(/^v/, "") === "24.22.2"
243
+ ? "ok"
244
+ : supported
245
+ ? "warning"
246
+ : "error",
247
+ message:
248
+ nodeVersion.replace(/^v/, "") === "24.22.2"
249
+ ? "Node matches the exact 24.22.2 package engine pin."
250
+ : supported
251
+ ? "Node differs from the exact 24.22.2 package engine pin; compatibility is unverified."
252
+ : "Node is older than the pinned 24.22.2 runtime or its version is invalid.",
253
+ action:
254
+ "Use the repository-pinned Node 24.22.2 for reproducible results.",
255
+ },
256
+ await projectConfig(cwd, configInspector),
257
+ ];
258
+ const result = {
259
+ schemaVersion: "1",
260
+ checks,
261
+ node: {
262
+ version: match ? match.slice(1).join(".") : null,
263
+ required: "24.22.2",
264
+ },
265
+ service: { origin },
266
+ limitations: [
267
+ "Diagnostics do not assess package safety or make a model request.",
268
+ "Only the nonsecret UNMASK_SERVER_URL environment value is consulted; shell startup files are not read and active shell guard interception is unverified.",
269
+ ],
270
+ };
271
+ if (offline)
272
+ checks.push({
273
+ id: "service",
274
+ status: "skipped",
275
+ message: "Hosted diagnostics skipped explicitly.",
276
+ action: "Run doctor without --offline to check the service.",
277
+ });
278
+ else {
279
+ try {
280
+ const version = await serviceJson(origin, "/version", {
281
+ fetchImpl,
282
+ signal,
283
+ });
284
+ if (
285
+ typeof version?.version !== "string" ||
286
+ !/^\d+\.\d+\.\d+(?:[-+][a-zA-Z0-9.-]+)?$/.test(version.version) ||
287
+ version.version.length > 100 ||
288
+ !/^[a-f0-9]{40}$/.test(version.commit) ||
289
+ !/^[a-f0-9]{64}$/.test(version.tarballSha256)
290
+ )
291
+ throw new Error();
292
+ result.service = {
293
+ origin,
294
+ version: version.version,
295
+ commit: version.commit,
296
+ tarballSha256: version.tarballSha256,
297
+ };
298
+ checks.push({
299
+ id: "service",
300
+ status: "ok",
301
+ message: "Hosted version endpoint responded with valid metadata.",
302
+ action:
303
+ "A completed package check is still required to verify the analysis path.",
304
+ });
305
+ } catch {
306
+ checks.push({
307
+ id: "service",
308
+ status: "error",
309
+ message:
310
+ "Hosted version diagnostics unavailable or invalid within bounded limits.",
311
+ action:
312
+ "Check connectivity and the explicit --server-url origin; retry doctor. No package was submitted.",
313
+ });
314
+ }
315
+ }
316
+ checks.push({
317
+ id: "guard",
318
+ status: "unknown",
319
+ message:
320
+ "Active shell guard integration cannot be verified from a child process.",
321
+ action:
322
+ "In a build with guard commands, use unmask enable for explicit setup, then run type npm in the invoking shell to verify interception.",
323
+ });
324
+ return result;
325
+ }
326
+ export async function doctorCommand(argv, runtime = {}) {
327
+ const options = parse(argv);
328
+ const result = await inspectDoctor({
329
+ cwd: options["--project"] ?? runtime.cwd,
330
+ nodeVersion: runtime.nodeVersion,
331
+ offline: !!options["--offline"],
332
+ serverUrl:
333
+ options["--server-url"] ?? (runtime.env ?? process.env).UNMASK_SERVER_URL,
334
+ fetchImpl: runtime.fetchImpl,
335
+ signal: runtime.signal,
336
+ configInspector: runtime.configInspector,
337
+ });
338
+ const stdout = runtime.stdout ?? process.stdout;
339
+ stdout.write(
340
+ options["--json"]
341
+ ? JSON.stringify(result, null, 2) + "\n"
342
+ : `UNMASK doctor\nNode ${result.node.version ?? "unknown"}; required ${result.node.required}\nService ${result.service.version ?? "unverified"} at ${result.service.origin}\n` +
343
+ result.checks
344
+ .map(
345
+ (check) =>
346
+ `${check.id}: ${check.status} — ${check.message}\n ${check.action}`,
347
+ )
348
+ .join("\n") +
349
+ "\n" +
350
+ result.limitations.join("\n") +
351
+ "\n",
352
+ );
353
+ return result.checks.some((check) => check.status === "error") ? 1 : 0;
354
+ }
@@ -0,0 +1,170 @@
1
+ import {
2
+ GuardConfigError,
3
+ activeExceptionFor,
4
+ exactPackage,
5
+ exceptionStatuses,
6
+ newException,
7
+ readGuardConfig,
8
+ writeGuardConfig,
9
+ } from "../core/guard-config.mjs";
10
+ import { parseCommandOptions } from "./config.mjs";
11
+
12
+ const USAGE =
13
+ "Use exceptions list [--json], exceptions check NAME@VERSION [--json], exceptions add NAME@VERSION --reason TEXT --expires YYYY-MM-DD, exceptions remove NAME@VERSION or exceptions prune; each accepts --project DIR";
14
+ const SCOPE =
15
+ "Exceptions apply only to the exact name@version listed, only until expiry, never to ranges, tags or wildcards. In enforcing profiles (strict, balanced) they never waive known malware; the audit profile blocks nothing.";
16
+
17
+ const line = (entry) =>
18
+ `${entry.name}@${entry.version} ${entry.status === "active" ? "active" : "EXPIRED (not applied)"} expires=${entry.expires} created=${entry.created} reason=${JSON.stringify(entry.reason)}`;
19
+
20
+ function audit(stdout, action, entry, file) {
21
+ stdout.write(
22
+ `audit: ${action} exception ${entry.name}@${entry.version} expires=${entry.expires} reason=${JSON.stringify(entry.reason)} file=${file}\n`,
23
+ );
24
+ }
25
+
26
+ export async function runExceptions(args, runtime = {}) {
27
+ const stdout = runtime.stdout ?? process.stdout;
28
+ const stderr = runtime.stderr ?? process.stderr;
29
+ const now = runtime.now ? runtime.now() : new Date();
30
+ try {
31
+ const { positional, options } = parseCommandOptions(
32
+ args,
33
+ ["--json"],
34
+ ["--project", "--reason", "--expires"],
35
+ );
36
+ const [action, spec, ...extra] = positional;
37
+ const project = options.project ?? runtime.cwd ?? process.cwd();
38
+ const mutation =
39
+ options.reason !== undefined || options.expires !== undefined;
40
+ if (extra.length || (action !== "add" && mutation))
41
+ throw new GuardConfigError("usage", USAGE);
42
+ if (action === "list" && !spec) {
43
+ const loaded = await readGuardConfig(project, { now });
44
+ const entries = exceptionStatuses(loaded.config, now);
45
+ if (options.json)
46
+ stdout.write(
47
+ JSON.stringify(
48
+ {
49
+ path: loaded.file,
50
+ source: loaded.source,
51
+ checkedAt: now.toISOString(),
52
+ scope: SCOPE,
53
+ exceptions: entries,
54
+ },
55
+ null,
56
+ 2,
57
+ ) + "\n",
58
+ );
59
+ else
60
+ stdout.write(
61
+ [
62
+ `Guard exceptions: ${loaded.file} (${entries.filter((e) => e.status === "active").length} active, ${entries.filter((e) => e.status === "expired").length} expired)`,
63
+ ...entries.map((entry) => ` ${line(entry)}`),
64
+ SCOPE,
65
+ ].join("\n") + "\n",
66
+ );
67
+ return 0;
68
+ }
69
+ if (action === "prune" && !spec && !options.json) {
70
+ const loaded = await readGuardConfig(project, { now });
71
+ const current = loaded.config.exceptions ?? [];
72
+ const expired = exceptionStatuses(loaded.config, now).filter(
73
+ (entry) => entry.status === "expired",
74
+ );
75
+ const saved = expired.length
76
+ ? await writeGuardConfig(
77
+ project,
78
+ {
79
+ ...loaded.config,
80
+ exceptions: current.filter(
81
+ (entry) => Date.parse(entry.expires) > now.getTime(),
82
+ ),
83
+ },
84
+ { now },
85
+ )
86
+ : loaded;
87
+ for (const entry of expired) audit(stdout, "pruned", entry, saved.file);
88
+ stdout.write(`Pruned ${expired.length} expired exception(s)\n`);
89
+ return 0;
90
+ }
91
+ if (action === "check" && spec) {
92
+ const identity = exactPackage(spec);
93
+ const loaded = await readGuardConfig(project, { now });
94
+ const entry = activeExceptionFor(loaded.config, identity, { now });
95
+ const result = {
96
+ package: `${identity.name}@${identity.version}`,
97
+ exception: entry,
98
+ knownMalwareWaived: false,
99
+ scope: SCOPE,
100
+ };
101
+ stdout.write(
102
+ options.json
103
+ ? JSON.stringify(result, null, 2) + "\n"
104
+ : `${entry ? `Active exception: ${line(entry)}` : `No active exception for ${result.package}`}\n${SCOPE}\n`,
105
+ );
106
+ return 0;
107
+ }
108
+ if (options.json || !spec) throw new GuardConfigError("usage", USAGE);
109
+ if (action === "add") {
110
+ if (options.reason === undefined || options.expires === undefined)
111
+ throw new GuardConfigError(
112
+ "usage",
113
+ "Exceptions require --reason TEXT and --expires YYYY-MM-DD",
114
+ );
115
+ const entry = newException(spec, options.reason, options.expires, now);
116
+ const loaded = await readGuardConfig(project, { now });
117
+ const current = loaded.config.exceptions ?? [];
118
+ if (
119
+ current.some(
120
+ (e) => e.name === entry.name && e.version === entry.version,
121
+ )
122
+ )
123
+ throw new GuardConfigError(
124
+ "duplicate-exception",
125
+ `An exception for ${spec} already exists; remove it first`,
126
+ );
127
+ const saved = await writeGuardConfig(
128
+ project,
129
+ {
130
+ ...loaded.config,
131
+ exceptions: [...current, entry],
132
+ },
133
+ { now },
134
+ );
135
+ audit(stdout, "added", entry, saved.file);
136
+ return 0;
137
+ }
138
+ if (action === "remove") {
139
+ const { name, version } = exactPackage(spec);
140
+ const loaded = await readGuardConfig(project, { now });
141
+ const current = loaded.config.exceptions ?? [];
142
+ const entry = current.find(
143
+ (e) => e.name === name && e.version === version,
144
+ );
145
+ if (!entry)
146
+ throw new GuardConfigError(
147
+ "missing-exception",
148
+ `No exception exists for ${name}@${version}`,
149
+ );
150
+ const saved = await writeGuardConfig(
151
+ project,
152
+ {
153
+ ...loaded.config,
154
+ exceptions: current.filter((e) => e !== entry),
155
+ },
156
+ { now },
157
+ );
158
+ audit(stdout, "removed", entry, saved.file);
159
+ return 0;
160
+ }
161
+ throw new GuardConfigError("usage", USAGE);
162
+ } catch (error) {
163
+ if (error instanceof GuardConfigError) {
164
+ stderr.write(`${error.code}: ${error.message}\n`);
165
+ return 2;
166
+ }
167
+ stderr.write("Guard exception error; no change is claimed.\n");
168
+ return 1;
169
+ }
170
+ }