@xagents_in/unmask 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/README.md +301 -85
  2. package/dist/cli/annotations.mjs +145 -0
  3. package/dist/cli/audit.mjs +223 -0
  4. package/dist/cli/batch.mjs +218 -0
  5. package/dist/cli/cache.mjs +488 -0
  6. package/dist/cli/compare.mjs +261 -0
  7. package/dist/cli/config.mjs +141 -0
  8. package/dist/cli/discovery.mjs +372 -0
  9. package/dist/cli/doctor.mjs +354 -0
  10. package/dist/cli/exceptions.mjs +170 -0
  11. package/dist/cli/file.mjs +271 -0
  12. package/dist/cli/files.mjs +141 -0
  13. package/dist/cli/history.mjs +232 -0
  14. package/dist/cli/hosted.mjs +251 -43
  15. package/dist/cli/info.mjs +125 -0
  16. package/dist/cli/init.mjs +57 -0
  17. package/dist/cli/install-guard.mjs +896 -0
  18. package/dist/cli/lockfile.mjs +330 -0
  19. package/dist/cli/main.mjs +101 -14
  20. package/dist/cli/preflight.mjs +388 -0
  21. package/dist/cli/sarif.mjs +100 -0
  22. package/dist/cli/scan.mjs +128 -0
  23. package/dist/cli/shell-integration.mjs +122 -0
  24. package/dist/cli/terminal.mjs +396 -0
  25. package/dist/cli/watch.mjs +233 -0
  26. package/dist/cli/why.mjs +256 -0
  27. package/dist/core/advisories.mjs +101 -24
  28. package/dist/core/advisory-cache.mjs +295 -0
  29. package/dist/core/behavior-signals.mjs +1088 -0
  30. package/dist/core/dependency-graph.mjs +332 -0
  31. package/dist/core/evidence-exports.mjs +469 -0
  32. package/dist/core/guard-config.mjs +463 -0
  33. package/dist/core/guard-policy.mjs +308 -0
  34. package/dist/core/package-details.mjs +42 -6
  35. package/dist/core/registry-facts.mjs +342 -0
  36. package/dist/core/report-exploration.mjs +328 -0
  37. package/dist/core/report.mjs +7 -0
  38. package/dist/core/sarif.mjs +556 -0
  39. package/dist/core/syntax.mjs +296 -15
  40. package/dist/model/client.mjs +204 -56
  41. package/dist/model/diff-evidence.mjs +177 -0
  42. package/dist/model/summary.mjs +183 -0
  43. package/dist/server/advisory-result.mjs +137 -0
  44. package/dist/server/app.mjs +66 -2
  45. package/dist/server/dependency-screening.mjs +386 -0
  46. package/dist/server/exports.mjs +63 -0
  47. package/dist/server/guard-decision.mjs +37 -0
  48. package/dist/server/main.mjs +25 -7
  49. package/dist/server/queue.mjs +95 -53
  50. package/dist/server/registry-facts.mjs +118 -0
  51. package/dist/server/report-exploration.mjs +87 -0
  52. package/dist/server/store.mjs +21 -1
  53. package/dist/version.json +1 -1
  54. package/dist/viewer/render.mjs +10 -2
  55. package/dist/viewer/shell.mjs +1 -1
  56. package/package.json +1 -1
@@ -0,0 +1,223 @@
1
+ import { LocalInputError } from "../core/pipeline.mjs";
2
+ import { AcquisitionError } from "../core/acquire.mjs";
3
+ import { checkHosted } from "./hosted.mjs";
4
+ import { exactTargets, readLockfile } from "./lockfile.mjs";
5
+
6
+ export const AUDIT_LIMITS = Object.freeze({
7
+ defaultMax: 6,
8
+ maxPackages: 50,
9
+ maxConcurrency: 2,
10
+ });
11
+ const STATUS_BY_EXIT = { 0: "no-findings", 2: "incomplete", 3: "findings" };
12
+
13
+ export function bindingStatus(target, report) {
14
+ if (!target.bound) return "unbound";
15
+ const actual = report?.inputs?.artifact?.sha512;
16
+ if (typeof actual !== "string" || !/^[a-f0-9]{128}$/.test(actual))
17
+ return "unavailable";
18
+ const locked = target.integrity
19
+ .flatMap((value) => value.split(/\s+/))
20
+ .filter((value) => value.startsWith("sha512-"))
21
+ .map((value) => Buffer.from(value.slice(7), "base64").toString("hex"));
22
+ return locked.length > 0 && locked.every((value) => value === actual)
23
+ ? "verified"
24
+ : "mismatch";
25
+ }
26
+
27
+ function positive(value, max, label) {
28
+ if (!/^[1-9]\d*$/.test(value ?? "") || Number(value) > max)
29
+ throw new LocalInputError(
30
+ "invalid-option",
31
+ `${label} must be an integer from 1 to ${max}.`,
32
+ );
33
+ return Number(value);
34
+ }
35
+
36
+ export function parseAuditArgs(argv) {
37
+ const options = {
38
+ lockfile: "package-lock.json",
39
+ max: AUDIT_LIMITS.defaultMax,
40
+ concurrency: 1,
41
+ omitDev: false,
42
+ json: false,
43
+ };
44
+ const seen = new Set();
45
+ for (let index = 0; index < argv.length; index++) {
46
+ const key = argv[index];
47
+ if (seen.has(key))
48
+ throw new LocalInputError(
49
+ "duplicate-option",
50
+ "Each option may be supplied only once",
51
+ );
52
+ seen.add(key);
53
+ if (key === "--json") options.json = true;
54
+ else if (key === "--omit-dev") options.omitDev = true;
55
+ else if (key === "--lockfile") {
56
+ const value = argv[++index];
57
+ if (!value || value.startsWith("--"))
58
+ throw new LocalInputError(
59
+ "missing-value",
60
+ "An option is missing its value",
61
+ );
62
+ options.lockfile = value;
63
+ } else if (key === "--max")
64
+ options.max = positive(argv[++index], AUDIT_LIMITS.maxPackages, "--max");
65
+ else if (key === "--concurrency")
66
+ options.concurrency = positive(
67
+ argv[++index],
68
+ AUDIT_LIMITS.maxConcurrency,
69
+ "--concurrency",
70
+ );
71
+ else
72
+ throw new LocalInputError(
73
+ "unknown-option",
74
+ "Use audit [--lockfile PATH] [--max N] [--concurrency N] [--omit-dev] [--json]",
75
+ );
76
+ }
77
+ return options;
78
+ }
79
+
80
+ async function checkOne(target, check) {
81
+ try {
82
+ const report = await check(target.spec);
83
+ const exitCode = report?.policy?.exitCode;
84
+ return {
85
+ ...target,
86
+ status: STATUS_BY_EXIT[exitCode] ?? "incomplete",
87
+ binding: bindingStatus(target, report),
88
+ exitCode,
89
+ findings: (report.policy.findings ?? []).map((item) => item.ruleId),
90
+ model: report.model?.status ?? "not-reported",
91
+ };
92
+ } catch (error) {
93
+ return {
94
+ ...target,
95
+ status: "failed",
96
+ error:
97
+ error instanceof LocalInputError
98
+ ? { code: error.code, message: error.message }
99
+ : error instanceof AcquisitionError
100
+ ? { code: error.code, message: "Invalid exact package identity." }
101
+ : { code: "internal", message: "Check failed unexpectedly." },
102
+ };
103
+ }
104
+ }
105
+
106
+ export async function auditLock(
107
+ lock,
108
+ { check, max = AUDIT_LIMITS.defaultMax, concurrency = 1, omitDev = false },
109
+ ) {
110
+ const { considered, targets, skipped, omitted } = exactTargets(lock, {
111
+ omitDev,
112
+ });
113
+ const selected = targets.slice(0, max);
114
+ const results = new Array(selected.length);
115
+ let next = 0;
116
+ // Bounded worker pool keeps hosted load within the shared rate limit.
117
+ await Promise.all(
118
+ Array.from({ length: Math.min(concurrency, selected.length) }, async () => {
119
+ while (next < selected.length) {
120
+ const index = next++;
121
+ results[index] = await checkOne(selected[index], check);
122
+ }
123
+ }),
124
+ );
125
+ const notChecked = targets
126
+ .slice(max)
127
+ .map((target) => ({ ...target, status: "not-checked" }));
128
+ const counts = {};
129
+ for (const item of [...results, ...notChecked])
130
+ counts[item.status] = (counts[item.status] ?? 0) + 1;
131
+ const skippedTotal = Object.values(skipped).reduce((a, b) => a + b, 0);
132
+ const binding = {};
133
+ for (const item of results)
134
+ if (item.binding) binding[item.binding] = (binding[item.binding] ?? 0) + 1;
135
+ const complete =
136
+ skippedTotal === 0 &&
137
+ notChecked.length === 0 &&
138
+ results.every(
139
+ (item) =>
140
+ ["no-findings", "findings"].includes(item.status) &&
141
+ item.binding === "verified",
142
+ );
143
+ return {
144
+ schemaVersion: "1",
145
+ lockfileVersion: lock.lockfileVersion,
146
+ scope: omitDev ? "production-and-optional" : "all-locked",
147
+ coverage: {
148
+ complete,
149
+ lockedEntries: considered,
150
+ uniqueExact: targets.length,
151
+ checked: results.length,
152
+ counts,
153
+ binding,
154
+ skipped,
155
+ omitted,
156
+ },
157
+ exitCode: counts.findings ? 3 : complete ? 0 : 2,
158
+ results: [...results, ...notChecked],
159
+ limitations: [
160
+ "No findings means no deterministic policy finding in the checked scope, not that a package is safe.",
161
+ "Removed or unpublished registry versions fail acquisition and are reported as failed, not clean.",
162
+ "Git, URL, file, workspace and invalid entries are not audited by exact registry identity.",
163
+ "Coverage requires each locked sha512 integrity to equal the hosted artifact sha512; unbound (no registry.npmjs.org resolved or sha512 integrity), mismatched or unreported hashes stay incomplete.",
164
+ "Unlocked optionalDependencies and optional peers are counted under coverage.omitted and not audited; npm does not require them, so they do not make coverage incomplete.",
165
+ "At most --max (default 6, matching the hosted limit of six new checks per ten minutes) unique versions are checked, in name order; the rest are not-checked.",
166
+ ],
167
+ };
168
+ }
169
+
170
+ function formatText(result) {
171
+ const lines = [];
172
+ const { coverage } = result;
173
+ lines.push(
174
+ `Audited ${coverage.checked} of ${coverage.uniqueExact} unique exact versions (${coverage.lockedEntries} locked entries, lockfile v${result.lockfileVersion}).`,
175
+ );
176
+ for (const item of result.results) {
177
+ const detail =
178
+ item.status === "failed"
179
+ ? ` ${item.error.code}`
180
+ : (item.findings?.length ? ` ${item.findings.join(",")}` : "") +
181
+ (item.binding && item.binding !== "verified"
182
+ ? ` [binding ${item.binding}]`
183
+ : "");
184
+ const alias = item.aliases?.length
185
+ ? ` (as ${item.aliases.join(", ")})`
186
+ : "";
187
+ lines.push(` ${item.status.padEnd(12)} ${item.spec}${alias}${detail}`);
188
+ }
189
+ for (const [reason, count] of Object.entries(coverage.skipped))
190
+ lines.push(` skipped ${count} entries: ${reason}`);
191
+ const { optional, peerOptional, devEntries, devEdges } = coverage.omitted;
192
+ if (optional + peerOptional + devEntries + devEdges > 0)
193
+ lines.push(
194
+ ` omitted ${optional} unlocked optional, ${peerOptional} unlocked optional peer edges; ${devEntries} dev entries, ${devEdges} dev edges (--omit-dev)`,
195
+ );
196
+ lines.push(
197
+ coverage.complete
198
+ ? "Coverage: complete for the checked scope."
199
+ : "Coverage: INCOMPLETE; unchecked, failed or skipped entries are not verified.",
200
+ );
201
+ lines.push(result.limitations[0]);
202
+ return lines.join("\n") + "\n";
203
+ }
204
+
205
+ export async function runAudit(argv, runtime = {}) {
206
+ const options = parseAuditArgs(argv);
207
+ const lock = await readLockfile(options.lockfile);
208
+ const env = runtime.env ?? process.env;
209
+ const check =
210
+ runtime.auditCheck ??
211
+ ((spec) =>
212
+ checkHosted(spec, {
213
+ serverUrl: env.UNMASK_SERVER_URL,
214
+ fetchImpl: runtime.fetchImpl,
215
+ signal: runtime.signal,
216
+ limits: runtime.hostedLimits,
217
+ }));
218
+ const result = await auditLock(lock, { ...options, check });
219
+ process.stdout.write(
220
+ options.json ? JSON.stringify(result, null, 2) + "\n" : formatText(result),
221
+ );
222
+ return result.exitCode;
223
+ }
@@ -0,0 +1,218 @@
1
+ import { parsePackageSpec } from "../core/acquire.mjs";
2
+ import { LocalInputError } from "../core/pipeline.mjs";
3
+ import { checkHosted } from "./hosted.mjs";
4
+
5
+ const failure = (code, message) => new LocalInputError(code, message);
6
+ const MAX_ITEMS = 25;
7
+ const MAX_INPUT_BYTES = 16384;
8
+ const terminalValue = (value) =>
9
+ JSON.stringify(value).replace(
10
+ /[\u007f-\u009f\u2028\u2029]/gu,
11
+ (character) =>
12
+ `\\u${character.charCodeAt(0).toString(16).padStart(4, "0")}`,
13
+ );
14
+
15
+ async function readInput(stdin, signal) {
16
+ const controller = new AbortController();
17
+ const timer = setTimeout(() => controller.abort(), 5000);
18
+ const lifetime = signal
19
+ ? AbortSignal.any([signal, controller.signal])
20
+ : controller.signal;
21
+ let onAbort;
22
+ try {
23
+ const aborted = new Promise((_resolve, reject) => {
24
+ onAbort = () =>
25
+ reject(
26
+ failure(
27
+ "batch-input-cancelled",
28
+ "Batch stdin timed out or was cancelled before EOF.",
29
+ ),
30
+ );
31
+ lifetime.addEventListener("abort", onAbort, { once: true });
32
+ if (lifetime.aborted) onAbort();
33
+ });
34
+ const reading = (async () => {
35
+ const chunks = [];
36
+ let size = 0;
37
+ for await (const chunk of stdin) {
38
+ const bytes = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
39
+ if (!(bytes instanceof Uint8Array))
40
+ throw failure("batch-input", "Batch stdin must contain UTF-8 text.");
41
+ size += bytes.byteLength;
42
+ if (size > MAX_INPUT_BYTES)
43
+ throw failure("batch-input-limit", "Batch stdin exceeds 16 KiB.");
44
+ chunks.push(bytes);
45
+ }
46
+ try {
47
+ return new TextDecoder("utf-8", { fatal: true }).decode(
48
+ Buffer.concat(chunks, size),
49
+ );
50
+ } catch {
51
+ throw failure(
52
+ "batch-input",
53
+ "Batch stdin must contain valid UTF-8 text.",
54
+ );
55
+ }
56
+ })();
57
+ return await Promise.race([reading, aborted]);
58
+ } catch (error) {
59
+ if (error instanceof LocalInputError) throw error;
60
+ throw failure("batch-input", "Batch stdin could not be read.");
61
+ } finally {
62
+ clearTimeout(timer);
63
+ lifetime.removeEventListener("abort", onAbort);
64
+ if (lifetime.aborted) stdin.destroy?.();
65
+ }
66
+ }
67
+
68
+ async function inputs(argv, runtime) {
69
+ const specs = [];
70
+ const seen = new Set();
71
+ for (const arg of argv) {
72
+ if (arg.startsWith("--")) {
73
+ if (!["--stdin", "--json"].includes(arg) || seen.has(arg))
74
+ throw failure(
75
+ "usage",
76
+ "Use batch exact-spec... [--stdin] [--json], with each option once.",
77
+ );
78
+ seen.add(arg);
79
+ } else specs.push(arg);
80
+ }
81
+ if (specs.length > MAX_ITEMS || specs.some((spec) => spec.length > 512))
82
+ throw failure(
83
+ "batch-input-limit",
84
+ "Batch accepts at most 25 inputs of at most 512 characters each.",
85
+ );
86
+ if (seen.has("--stdin")) {
87
+ const text = await readInput(
88
+ runtime.stdin ?? process.stdin,
89
+ runtime.signal,
90
+ );
91
+ specs.push(...text.split(/\s+/u).filter(Boolean));
92
+ }
93
+ if (!specs.length)
94
+ throw failure(
95
+ "usage",
96
+ "Batch requires explicit exact specs from args or --stdin.",
97
+ );
98
+ if (specs.length > MAX_ITEMS || specs.some((spec) => spec.length > 512))
99
+ throw failure(
100
+ "batch-input-limit",
101
+ "Batch accepts at most 25 inputs of at most 512 characters each.",
102
+ );
103
+ return { specs, json: seen.has("--json") };
104
+ }
105
+
106
+ export async function runBatch(argv, runtime = {}) {
107
+ const { specs, json } = await inputs(argv, runtime);
108
+ const unique = new Map();
109
+ for (const [index, spec] of specs.entries()) {
110
+ if (!unique.has(spec)) unique.set(spec, { spec, inputIndexes: [] });
111
+ unique.get(spec).inputIndexes.push(index);
112
+ }
113
+ const controller = new AbortController();
114
+ const timer = setTimeout(() => controller.abort(), 600000);
115
+ const lifetime = runtime.signal
116
+ ? AbortSignal.any([runtime.signal, controller.signal])
117
+ : controller.signal;
118
+ const items = [];
119
+ let paused = false;
120
+ try {
121
+ for (const item of unique.values()) {
122
+ const result = { ...item, occurrences: item.inputIndexes.length };
123
+ try {
124
+ parsePackageSpec(item.spec);
125
+ } catch {
126
+ items.push({
127
+ ...result,
128
+ status: "error",
129
+ error: {
130
+ code: "invalid-package-spec",
131
+ message:
132
+ "Use an exact public npm package name@version; ranges, tags and URLs are unsupported.",
133
+ },
134
+ });
135
+ continue;
136
+ }
137
+ if (lifetime.aborted || paused) {
138
+ items.push({
139
+ ...result,
140
+ status: "not-checked",
141
+ error: {
142
+ code: lifetime.aborted
143
+ ? "batch-cancelled"
144
+ : "batch-submission-paused",
145
+ message: "No hosted request was submitted for this item.",
146
+ },
147
+ });
148
+ continue;
149
+ }
150
+ try {
151
+ const report = await checkHosted(item.spec, {
152
+ serverUrl: (runtime.env ?? process.env).UNMASK_SERVER_URL,
153
+ fetchImpl: runtime.fetchImpl,
154
+ signal: lifetime,
155
+ limits: runtime.hostedLimits,
156
+ });
157
+ items.push({
158
+ ...result,
159
+ status: "completed",
160
+ policyExitCode: report.policy.exitCode,
161
+ findingCount: report.policy.findings.length,
162
+ model: {
163
+ status: String(report.model?.status ?? "unavailable").slice(0, 80),
164
+ },
165
+ });
166
+ } catch (error) {
167
+ const known = error instanceof LocalInputError;
168
+ items.push({
169
+ ...result,
170
+ status: "error",
171
+ error: {
172
+ code: known ? error.code : "batch-check-failed",
173
+ message: known
174
+ ? error.message
175
+ : "Check failed; no completed result is claimed.",
176
+ },
177
+ });
178
+ if (
179
+ ["hosted-submit-failed", "hosted-report-failed"].includes(error?.code)
180
+ )
181
+ paused = true;
182
+ }
183
+ }
184
+ } finally {
185
+ clearTimeout(timer);
186
+ }
187
+ const result = {
188
+ schemaVersion: "1",
189
+ requestedCount: specs.length,
190
+ uniqueCount: items.length,
191
+ duplicateCount: specs.length - items.length,
192
+ completedCount: items.filter((item) => item.status === "completed").length,
193
+ errorCount: items.filter((item) => item.status === "error").length,
194
+ notCheckedCount: items.filter((item) => item.status === "not-checked")
195
+ .length,
196
+ items,
197
+ };
198
+ const exitCode =
199
+ result.errorCount || result.notCheckedCount
200
+ ? 2
201
+ : items.some((item) => item.policyExitCode === 3)
202
+ ? 3
203
+ : items.some((item) => item.policyExitCode === 2)
204
+ ? 2
205
+ : 0;
206
+ const stdout = runtime.stdout ?? process.stdout;
207
+ if (json) stdout.write(JSON.stringify({ ...result, exitCode }) + "\n");
208
+ else {
209
+ for (const item of items)
210
+ stdout.write(
211
+ `${terminalValue(item.spec)} x${item.occurrences}: ${item.status}${item.status === "completed" ? `; policy exit ${item.policyExitCode}; findings ${item.findingCount}; AI status ${terminalValue(item.model.status)}` : `; ${item.error.code}`}\n`,
212
+ );
213
+ stdout.write(
214
+ `Batch: ${result.completedCount} completed, ${result.errorCount} errors, ${result.notCheckedCount} not checked; ${result.duplicateCount} duplicate inputs coalesced.\nNo findings do not establish safety.\n`,
215
+ );
216
+ }
217
+ return exitCode;
218
+ }